From a9756e4a64a29dfb2086e418415149b2446ae931 Mon Sep 17 00:00:00 2001 From: alpo Date: Sat, 15 Oct 2022 20:28:25 -0700 Subject: [PATCH 1/9] add tests for 10-asset pools with 10B per asset --- x/gamm/pool-models/stableswap/amm_test.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/x/gamm/pool-models/stableswap/amm_test.go b/x/gamm/pool-models/stableswap/amm_test.go index a5c1f1ec28a..361d7179d95 100644 --- a/x/gamm/pool-models/stableswap/amm_test.go +++ b/x/gamm/pool-models/stableswap/amm_test.go @@ -224,8 +224,8 @@ var ( yIn: osmomath.NewBigDec(1), expectPanic: false, }, - /* TODO: increase BigDec precision (36 -> 72) to be able to accommodate this - "even 4-asset large pool, small input": { + // TODO: increase BigDec precision (36 -> 72) to be able to accommodate this + "even 4-asset large pool (100M each), small input": { xReserve: osmomath.NewBigDec(100000000), yReserve: osmomath.NewBigDec(100000000), // represents a 4-asset pool with 100M in each reserve @@ -233,7 +233,22 @@ var ( yIn: osmomath.NewBigDec(100), expectPanic: false, }, - */ + "even 4-asset max pool (10B each), small input": { + xReserve: osmomath.NewBigDec(10000000000), + yReserve: osmomath.NewBigDec(10000000000), + // represents a 4-asset pool with 10B in each reserve + remReserves: []osmomath.BigDec{osmomath.NewBigDec(10000000000), osmomath.NewBigDec(10000000000)}, + yIn: osmomath.NewBigDec(100000000), + expectPanic: false, + }, + "even 10-asset max pool (10B each), small input": { + xReserve: osmomath.NewBigDec(10_000_000_000), + yReserve: osmomath.NewBigDec(10_000_000_000), + // represents a 10-asset pool with 10B in each reserve + remReserves: []osmomath.BigDec{osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000)}, + yIn: osmomath.NewBigDec(100), + expectPanic: false, + }, // uneven pools "uneven 3-asset pool, even swap assets as pool minority": { From 687ceca4fa372590681160eb9cbf0fa01c3ad8d0 Mon Sep 17 00:00:00 2001 From: alpo Date: Wed, 19 Oct 2022 09:45:57 -0700 Subject: [PATCH 2/9] add max post-scaled asset check and create pool tests --- x/gamm/pool-models/stableswap/amm.go | 4 ++ x/gamm/pool-models/stableswap/amm_test.go | 21 +++++---- x/gamm/pool-models/stableswap/msgs.go | 13 +++--- x/gamm/pool-models/stableswap/msgs_test.go | 54 ++++++++++++++++++++++ x/gamm/pool-models/stableswap/pool.go | 33 +++++++++++++ x/gamm/types/errors.go | 3 +- 6 files changed, 112 insertions(+), 16 deletions(-) diff --git a/x/gamm/pool-models/stableswap/amm.go b/x/gamm/pool-models/stableswap/amm.go index 4619cc42759..8d744a326cd 100644 --- a/x/gamm/pool-models/stableswap/amm.go +++ b/x/gamm/pool-models/stableswap/amm.go @@ -346,5 +346,9 @@ func (p *Pool) joinPoolSharesInternal(ctx sdk.Context, tokensIn sdk.Coins, swapF numShares = numShares.Add(newShare) } + if err = validatePoolAssets(p.PoolLiquidity, p.ScalingFactor); err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + return numShares, tokensIn, nil } diff --git a/x/gamm/pool-models/stableswap/amm_test.go b/x/gamm/pool-models/stableswap/amm_test.go index 361d7179d95..6c926c469a0 100644 --- a/x/gamm/pool-models/stableswap/amm_test.go +++ b/x/gamm/pool-models/stableswap/amm_test.go @@ -224,29 +224,36 @@ var ( yIn: osmomath.NewBigDec(1), expectPanic: false, }, - // TODO: increase BigDec precision (36 -> 72) to be able to accommodate this "even 4-asset large pool (100M each), small input": { xReserve: osmomath.NewBigDec(100000000), yReserve: osmomath.NewBigDec(100000000), // represents a 4-asset pool with 100M in each reserve remReserves: []osmomath.BigDec{osmomath.NewBigDec(100000000), osmomath.NewBigDec(100000000)}, - yIn: osmomath.NewBigDec(100), + yIn: osmomath.NewBigDec(100), expectPanic: false, }, - "even 4-asset max pool (10B each), small input": { + "even 4-asset pool (10B each post-scaled), small input": { xReserve: osmomath.NewBigDec(10000000000), yReserve: osmomath.NewBigDec(10000000000), // represents a 4-asset pool with 10B in each reserve remReserves: []osmomath.BigDec{osmomath.NewBigDec(10000000000), osmomath.NewBigDec(10000000000)}, - yIn: osmomath.NewBigDec(100000000), + yIn: osmomath.NewBigDec(100000000), expectPanic: false, }, - "even 10-asset max pool (10B each), small input": { + "even 10-asset pool (10B each post-scaled), small input": { xReserve: osmomath.NewBigDec(10_000_000_000), yReserve: osmomath.NewBigDec(10_000_000_000), // represents a 10-asset pool with 10B in each reserve remReserves: []osmomath.BigDec{osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000), osmomath.NewBigDec(10_000_000_000)}, - yIn: osmomath.NewBigDec(100), + yIn: osmomath.NewBigDec(100), + expectPanic: false, + }, + "even 10-asset pool (100B each post-scaled), large input": { + xReserve: osmomath.NewBigDec(100_000_000_000), + yReserve: osmomath.NewBigDec(100_000_000_000), + // represents a 10-asset pool with 100B in each reserve + remReserves: []osmomath.BigDec{osmomath.NewBigDec(100_000_000_000), osmomath.NewBigDec(100_000_000_000), osmomath.NewBigDec(100_000_000_000), osmomath.NewBigDec(100_000_000_000), osmomath.NewBigDec(100_000_000_000), osmomath.NewBigDec(100_000_000_000), osmomath.NewBigDec(100_000_000_000), osmomath.NewBigDec(100_000_000_000)}, + yIn: osmomath.NewBigDec(10_000_000_000), expectPanic: false, }, @@ -692,8 +699,6 @@ func TestCalcSingleAssetJoinShares(t *testing.T) { swapFee: sdk.MustNewDecFromStr("0.03"), expectedOut: sdk.NewInt(100 - 3), }, - - // TODO: increase BigDec precision further to be able to accommodate 5-asset pool tests } for name, tc := range tests { diff --git a/x/gamm/pool-models/stableswap/msgs.go b/x/gamm/pool-models/stableswap/msgs.go index 359c91fb5f0..bf7d1368738 100644 --- a/x/gamm/pool-models/stableswap/msgs.go +++ b/x/gamm/pool-models/stableswap/msgs.go @@ -46,13 +46,6 @@ func (msg MsgCreateStableswapPool) ValidateBasic() error { return err } - // validation for pool initial liquidity - if len(msg.InitialPoolLiquidity) < 2 { - return types.ErrTooFewPoolAssets - } else if len(msg.InitialPoolLiquidity) > 8 { - return types.ErrTooManyPoolAssets - } - // validation for scaling factors // The message's scaling factors must be empty or a valid set of scaling factors if len(msg.ScalingFactors) != 0 { @@ -61,6 +54,12 @@ func (msg MsgCreateStableswapPool) ValidateBasic() error { } } + // validation for pool initial liquidity + // The message's pool liquidity must have between 2 and 8 assets with at most 10B post-scaled units in each + if err = validatePoolAssets(msg.InitialPoolLiquidity, msg.ScalingFactors); err != nil { + return err + } + // validation for scaling factor owner if err = validateScalingFactorController(msg.ScalingFactorController); err != nil { return err diff --git a/x/gamm/pool-models/stableswap/msgs_test.go b/x/gamm/pool-models/stableswap/msgs_test.go index aa3fcbd8e93..727e4dc3fa1 100644 --- a/x/gamm/pool-models/stableswap/msgs_test.go +++ b/x/gamm/pool-models/stableswap/msgs_test.go @@ -209,6 +209,60 @@ func TestMsgCreateStableswapPool(t *testing.T) { }), expectPass: true, }, + { + name: "max asset amounts", + msg: createMsg(func(msg stableswap.MsgCreateStableswapPool) stableswap.MsgCreateStableswapPool { + msg.InitialPoolLiquidity = sdk.Coins{ + sdk.NewCoin("osmo", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("atom", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("usdt", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("usdc", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("juno", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("akt", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("regen", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("band", sdk.NewInt(10_000_000_000)), + } + msg.ScalingFactors = []uint64{1, 1, 1, 1, 1, 1, 1, 1} + return msg + }), + expectPass: true, + }, + { + name: "greater than max post-scaled amount with regular scaling factors", + msg: createMsg(func(msg stableswap.MsgCreateStableswapPool) stableswap.MsgCreateStableswapPool { + msg.InitialPoolLiquidity = sdk.Coins{ + sdk.NewCoin("osmo", sdk.NewInt(1+10_000_000_000)), + sdk.NewCoin("atom", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("usdt", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("usdc", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("juno", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("akt", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("regen", sdk.NewInt(10_000_000_000)), + sdk.NewCoin("band", sdk.NewInt(10_000_000_000)), + } + msg.ScalingFactors = []uint64{1, 1, 1, 1, 1, 1, 1, 1} + return msg + }), + expectPass: false, + }, + { + name: "100B token 8-asset pool using large scaling factors", + msg: createMsg(func(msg stableswap.MsgCreateStableswapPool) stableswap.MsgCreateStableswapPool { + msg.InitialPoolLiquidity = sdk.Coins{ + sdk.NewCoin("osmo", sdk.NewInt(100_000_000_000_000_000)), + sdk.NewCoin("atom", sdk.NewInt(100_000_000_000_000_000)), + sdk.NewCoin("usdt", sdk.NewInt(100_000_000_000_000_000)), + sdk.NewCoin("usdc", sdk.NewInt(100_000_000_000_000_000)), + sdk.NewCoin("juno", sdk.NewInt(100_000_000_000_000_000)), + sdk.NewCoin("akt", sdk.NewInt(100_000_000_000_000_000)), + sdk.NewCoin("regen", sdk.NewInt(100_000_000_000_000_000)), + sdk.NewCoin("band", sdk.NewInt(100_000_000_000_000_000)), + } + msg.ScalingFactors = []uint64{10000000, 10000000, 10000000, 10000000, 10000000, 10000000, 10000000, 10000000} + return msg + }), + expectPass: true, + }, } for _, test := range tests { diff --git a/x/gamm/pool-models/stableswap/pool.go b/x/gamm/pool-models/stableswap/pool.go index 46660b4d750..769b782571c 100644 --- a/x/gamm/pool-models/stableswap/pool.go +++ b/x/gamm/pool-models/stableswap/pool.go @@ -37,6 +37,10 @@ func NewStableswapPool(poolId uint64, return Pool{}, err } + if err := validatePoolAssets(initialLiquidity, scalingFactors); err != nil { + return Pool{}, err + } + pool := Pool{ Address: types.NewPoolAddress(poolId).String(), Id: poolId, @@ -230,6 +234,10 @@ func (p Pool) CalcOutAmtGivenIn(ctx sdk.Context, tokenIn sdk.Coins, tokenOutDeno } func (p *Pool) SwapOutAmtGivenIn(ctx sdk.Context, tokenIn sdk.Coins, tokenOutDenom string, swapFee sdk.Dec) (tokenOut sdk.Coin, err error) { + if err = validatePoolAssets(p.PoolLiquidity.Add(tokenIn...), p.ScalingFactor); err != nil { + return sdk.Coin{}, err + } + tokenOut, err = p.CalcOutAmtGivenIn(ctx, tokenIn, tokenOutDenom, swapFee) if err != nil { return sdk.Coin{}, err @@ -266,6 +274,10 @@ func (p *Pool) SwapInAmtGivenOut(ctx sdk.Context, tokenOut sdk.Coins, tokenInDen return sdk.Coin{}, err } + if err = validatePoolAssets(p.PoolLiquidity.Add(tokenIn), p.ScalingFactor); err != nil { + return sdk.Coin{}, err + } + p.updatePoolLiquidityForSwap(sdk.NewCoins(tokenIn), tokenOut) return tokenIn, nil @@ -332,6 +344,10 @@ func (p *Pool) SetStableSwapScalingFactors(ctx sdk.Context, scalingFactors []uin return err } + if err := validatePoolAssets(p.PoolLiquidity, scalingFactors); err != nil { + return err + } + p.ScalingFactor = scalingFactors return nil } @@ -357,3 +373,20 @@ func validateScalingFactors(scalingFactors []uint64, numAssets int) error { return nil } + +func validatePoolAssets(initialAssets sdk.Coins, scalingFactors []uint64) error { + if len(initialAssets) < 2 { + return types.ErrTooFewPoolAssets + } else if len(initialAssets) > 8 { + return types.ErrTooManyPoolAssets + } + + maxScaledAmount := sdk.NewInt(10_000_000_000) + for i, asset := range initialAssets { + if asset.Amount.Quo(sdk.NewInt(int64(scalingFactors[i]))).GT(maxScaledAmount) { + return types.ErrHitMaxScaledAssets + } + } + + return nil +} diff --git a/x/gamm/types/errors.go b/x/gamm/types/errors.go index 44693c63d55..ce1828e5982 100644 --- a/x/gamm/types/errors.go +++ b/x/gamm/types/errors.go @@ -20,7 +20,7 @@ var ( ErrPoolAlreadyExist = sdkerrors.Register(ModuleName, 2, "pool already exist") ErrPoolLocked = sdkerrors.Register(ModuleName, 3, "pool is locked") ErrTooFewPoolAssets = sdkerrors.Register(ModuleName, 4, "pool should have at least 2 assets, as they must be swapping between at least two assets") - ErrTooManyPoolAssets = sdkerrors.Register(ModuleName, 5, "pool has too many assets (currently capped at 8 assets per balancer pool and 2 per stableswap)") + ErrTooManyPoolAssets = sdkerrors.Register(ModuleName, 5, "pool has too many assets (currently capped at 8 assets per pool)") ErrLimitMaxAmount = sdkerrors.Register(ModuleName, 6, "calculated amount is larger than max amount") ErrLimitMinAmount = sdkerrors.Register(ModuleName, 7, "calculated amount is lesser than min amount") ErrInvalidMathApprox = sdkerrors.Register(ModuleName, 8, "invalid calculated result") @@ -52,4 +52,5 @@ var ( ErrInvalidStableswapScalingFactors = sdkerrors.Register(ModuleName, 62, "length between liquidity and scaling factors mismatch") ErrNotScalingFactorGovernor = sdkerrors.Register(ModuleName, 63, "not scaling factor governor") ErrInvalidScalingFactors = sdkerrors.Register(ModuleName, 64, "invalid scaling factor") + ErrHitMaxScaledAssets = sdkerrors.Register(ModuleName, 65, "post-scaled pool assets can not exceed 10B") ) From ee2fb0cda8119086d1ee8c013aeb7b6f706ccadb Mon Sep 17 00:00:00 2001 From: alpo Date: Wed, 19 Oct 2022 11:11:29 -0700 Subject: [PATCH 3/9] add sanity tests for new swap guardrails --- x/gamm/pool-models/stableswap/pool_test.go | 134 +++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/x/gamm/pool-models/stableswap/pool_test.go b/x/gamm/pool-models/stableswap/pool_test.go index 1f48ca8534d..67e1d6f168e 100644 --- a/x/gamm/pool-models/stableswap/pool_test.go +++ b/x/gamm/pool-models/stableswap/pool_test.go @@ -505,3 +505,137 @@ func TestScaleCoin(t *testing.T) { }) } } + +func TestSwapOutAmtGivenIn(t *testing.T) { + tests := map[string]struct { + poolAssets sdk.Coins + scalingFactors []uint64 + tokenIn sdk.Coins + expectedTokenOut sdk.Coin + expectedPoolLiquidity sdk.Coins + swapFee sdk.Dec + expError bool + }{ + "even pool basic trade": { + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + tokenIn: sdk.NewCoins(sdk.NewInt64Coin("foo", 100)), + // we expect at least a 1 token difference since output is truncated + expectedTokenOut: sdk.NewInt64Coin("bar", 99), + expectedPoolLiquidity: twoEvenStablePoolAssets.Add(sdk.NewInt64Coin("foo", 100)).Sub(sdk.NewCoins(sdk.NewInt64Coin("bar", 99))), + swapFee: sdk.ZeroDec(), + expError: false, + }, + "trade hits max pool capacity for asset": { + poolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 9_999_999_998), + sdk.NewInt64Coin("bar", 9_999_999_999), + ), + scalingFactors: defaultTwoAssetScalingFactors, + tokenIn: sdk.NewCoins(sdk.NewInt64Coin("foo", 1)), + expectedTokenOut: sdk.NewInt64Coin("bar", 1), + expectedPoolLiquidity: sdk.NewCoins( + sdk.NewInt64Coin("foo", 9_999_999_999), + sdk.NewInt64Coin("bar", 9_999_999_998), + ), + swapFee: sdk.ZeroDec(), + expError: false, + }, + "trade exceeds max pool capacity for asset": { + poolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 10_000_000_000), + sdk.NewInt64Coin("bar", 10_000_000_000), + ), + scalingFactors: defaultTwoAssetScalingFactors, + tokenIn: sdk.NewCoins(sdk.NewInt64Coin("foo", 1)), + expectedTokenOut: sdk.Coin{}, + expectedPoolLiquidity: sdk.NewCoins( + sdk.NewInt64Coin("foo", 10_000_000_000), + sdk.NewInt64Coin("bar", 10_000_000_000), + ), + swapFee: sdk.ZeroDec(), + expError: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := sdk.Context{} + p := poolStructFromAssets(tc.poolAssets, tc.scalingFactors) + + tokenOut, err := p.SwapOutAmtGivenIn(ctx, tc.tokenIn, tc.expectedTokenOut.Denom, tc.swapFee) + if !tc.expError { + require.Equal(t, tc.expectedTokenOut, tokenOut) + require.Equal(t, tc.expectedPoolLiquidity, p.PoolLiquidity) + } + osmoassert.ConditionalError(t, tc.expError, err) + }) + } +} + +func TestSwapInAmtGivenOut(t *testing.T) { + tests := map[string]struct { + poolAssets sdk.Coins + scalingFactors []uint64 + tokenOut sdk.Coins + expectedTokenIn sdk.Coin + expectedPoolLiquidity sdk.Coins + swapFee sdk.Dec + expError bool + }{ + "even pool basic trade": { + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + tokenOut: sdk.NewCoins(sdk.NewInt64Coin("bar", 99)), + // we expect at least a 1 token difference from our true expected output since it is truncated + expectedTokenIn: sdk.NewInt64Coin("foo", 99), + expectedPoolLiquidity: twoEvenStablePoolAssets.Add(sdk.NewInt64Coin("foo", 99)).Sub(sdk.NewCoins(sdk.NewInt64Coin("bar", 99))), + swapFee: sdk.ZeroDec(), + expError: false, + }, + "trade hits max pool capacity for asset": { + poolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 9_999_999_998), + sdk.NewInt64Coin("bar", 9_999_999_999), + ), + scalingFactors: defaultTwoAssetScalingFactors, + tokenOut: sdk.NewCoins(sdk.NewInt64Coin("bar", 1)), + expectedTokenIn: sdk.NewInt64Coin("foo", 1), + expectedPoolLiquidity: sdk.NewCoins( + sdk.NewInt64Coin("foo", 9_999_999_999), + sdk.NewInt64Coin("bar", 9_999_999_998), + ), + swapFee: sdk.ZeroDec(), + expError: false, + }, + "trade exceeds max pool capacity for asset": { + poolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 10_000_000_000), + sdk.NewInt64Coin("bar", 10_000_000_000), + ), + scalingFactors: defaultTwoAssetScalingFactors, + tokenOut: sdk.NewCoins(sdk.NewInt64Coin("bar", 1)), + expectedTokenIn: sdk.Coin{}, + expectedPoolLiquidity: sdk.NewCoins( + sdk.NewInt64Coin("foo", 10_000_000_000), + sdk.NewInt64Coin("bar", 10_000_000_000), + ), + swapFee: sdk.ZeroDec(), + expError: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := sdk.Context{} + p := poolStructFromAssets(tc.poolAssets, tc.scalingFactors) + + tokenIn, err := p.SwapInAmtGivenOut(ctx, tc.tokenOut, tc.expectedTokenIn.Denom, tc.swapFee) + if !tc.expError { + require.Equal(t, tc.expectedTokenIn, tokenIn) + require.Equal(t, tc.expectedPoolLiquidity, p.PoolLiquidity) + } + osmoassert.ConditionalError(t, tc.expError, err) + }) + } +} From 30f1d64ab813db5489385d3585b192adfc6b91cc Mon Sep 17 00:00:00 2001 From: alpo Date: Wed, 19 Oct 2022 11:17:46 -0700 Subject: [PATCH 4/9] move max scaled asset amt to constant --- x/gamm/pool-models/stableswap/pool.go | 7 +++---- x/gamm/types/constants.go | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/x/gamm/pool-models/stableswap/pool.go b/x/gamm/pool-models/stableswap/pool.go index 769b782571c..9495b92c5ad 100644 --- a/x/gamm/pool-models/stableswap/pool.go +++ b/x/gamm/pool-models/stableswap/pool.go @@ -375,15 +375,14 @@ func validateScalingFactors(scalingFactors []uint64, numAssets int) error { } func validatePoolAssets(initialAssets sdk.Coins, scalingFactors []uint64) error { - if len(initialAssets) < 2 { + if len(initialAssets) < types.MinPoolAssets { return types.ErrTooFewPoolAssets - } else if len(initialAssets) > 8 { + } else if len(initialAssets) > types.MaxPoolAssets { return types.ErrTooManyPoolAssets } - maxScaledAmount := sdk.NewInt(10_000_000_000) for i, asset := range initialAssets { - if asset.Amount.Quo(sdk.NewInt(int64(scalingFactors[i]))).GT(maxScaledAmount) { + if asset.Amount.Quo(sdk.NewInt(int64(scalingFactors[i]))).GT(sdk.NewInt(types.StableswapMaxScaledAmtPerAsset)) { return types.ErrHitMaxScaledAssets } } diff --git a/x/gamm/types/constants.go b/x/gamm/types/constants.go index 76ad1efec91..4152661d7a8 100644 --- a/x/gamm/types/constants.go +++ b/x/gamm/types/constants.go @@ -13,6 +13,8 @@ const ( // i.e. SigFigExponent = 8 is 10^8 which is 100000000. This gives 8 significant figures. SigFigsExponent = 8 BalancerGasFeeForSwap = 10_000 + + StableswapMaxScaledAmtPerAsset = 10_000_000_000 ) var ( From 8974438b461949bb71e4c4f0de2d941d4c935ef0 Mon Sep 17 00:00:00 2001 From: alpo Date: Wed, 19 Oct 2022 12:55:32 -0700 Subject: [PATCH 5/9] add join-pool-internal tests for new functionality --- x/gamm/pool-models/stableswap/amm.go | 24 +++-- x/gamm/pool-models/stableswap/amm_test.go | 114 ++++++++++++++++++++++ 2 files changed, 130 insertions(+), 8 deletions(-) diff --git a/x/gamm/pool-models/stableswap/amm.go b/x/gamm/pool-models/stableswap/amm.go index 8d744a326cd..49d9c14bbbb 100644 --- a/x/gamm/pool-models/stableswap/amm.go +++ b/x/gamm/pool-models/stableswap/amm.go @@ -320,11 +320,14 @@ func (p *Pool) calcSingleAssetJoinShares(tokenIn sdk.Coin, swapFee sdk.Dec) (sdk // We can mutate pa here // TODO: some day switch this to a COW wrapped pa, for better perf func (p *Pool) joinPoolSharesInternal(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, newLiquidity sdk.Coins, err error) { - if len(tokensIn) == 1 { + if !tokensIn.DenomsSubsetOf(p.GetTotalPoolLiquidity(ctx)) { + return sdk.ZeroInt(), sdk.NewCoins(), errors.New("attempted joining pool with assets that do not exist in pool") + } + if len(tokensIn) == 1 && tokensIn[0].Amount.GT(sdk.OneInt()) { numShares, err = p.calcSingleAssetJoinShares(tokensIn[0], swapFee) newLiquidity = tokensIn return numShares, newLiquidity, err - } else if len(tokensIn) != p.NumAssets() || !tokensIn.DenomsSubsetOf(p.GetTotalPoolLiquidity(ctx)) { + } else if len(tokensIn) != p.NumAssets() { return sdk.ZeroInt(), sdk.NewCoins(), errors.New( "stableswap pool only supports LP'ing with one asset, or all assets in pool") } @@ -336,19 +339,24 @@ func (p *Pool) joinPoolSharesInternal(ctx sdk.Context, tokensIn sdk.Coins, swapF } p.updatePoolForJoin(tokensIn.Sub(remCoins), numShares) + tokensJoined := tokensIn for _, coin := range remCoins { // TODO: Perhaps add a method to skip if this is too small. - newShare, err := p.calcSingleAssetJoinShares(coin, swapFee) - if err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err + if coin.Amount.GT(sdk.OneInt()) { + newShare, err := p.calcSingleAssetJoinShares(coin, swapFee) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + p.updatePoolForJoin(sdk.NewCoins(coin), newShare) + numShares = numShares.Add(newShare) + } else { + tokensJoined = tokensJoined.Sub(sdk.NewCoins(coin)) } - p.updatePoolForJoin(sdk.NewCoins(coin), newShare) - numShares = numShares.Add(newShare) } if err = validatePoolAssets(p.PoolLiquidity, p.ScalingFactor); err != nil { return sdk.ZeroInt(), sdk.NewCoins(), err } - return numShares, tokensIn, nil + return numShares, tokensJoined, nil } diff --git a/x/gamm/pool-models/stableswap/amm_test.go b/x/gamm/pool-models/stableswap/amm_test.go index 6c926c469a0..0695c96c30e 100644 --- a/x/gamm/pool-models/stableswap/amm_test.go +++ b/x/gamm/pool-models/stableswap/amm_test.go @@ -722,3 +722,117 @@ func TestCalcSingleAssetJoinShares(t *testing.T) { }) } } + +func TestJoinPoolSharesInternal(t *testing.T) { + tenPercentOfTwoPoolRaw := int64(1000000000 / 10) + tenPercentOfTwoPoolCoins := sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(int64(1000000000/10))), sdk.NewCoin("bar", sdk.NewInt(int64(1000000000/10)))) + twoAssetPlusTenPercent := twoEvenStablePoolAssets.Add(tenPercentOfTwoPoolCoins...) + type testcase struct { + tokensIn sdk.Coins + poolAssets sdk.Coins + scalingFactors []uint64 + swapFee sdk.Dec + expNumShare sdk.Int + expTokensJoined sdk.Coins + expPoolAssets sdk.Coins + expectPass bool + } + + tests := map[string]testcase{ + "even two asset pool, same tokenIn ratio": { + tokensIn: tenPercentOfTwoPoolCoins, + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: tenPercentOfTwoPoolCoins, + expPoolAssets: twoAssetPlusTenPercent, + expectPass: true, + }, + "even two asset pool, different tokenIn ratio with pool": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPoolRaw)), sdk.NewCoin("bar", sdk.NewInt(10+tenPercentOfTwoPoolRaw))), + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expNumShare: sdk.NewIntFromUint64(10000000500000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPoolRaw)), sdk.NewCoin("bar", sdk.NewInt(10+tenPercentOfTwoPoolRaw))), + expPoolAssets: twoAssetPlusTenPercent.Add(sdk.NewCoin("bar", sdk.NewInt(10))), + expectPass: true, + }, + "all-asset pool join attempt exceeds max scaled asset amount": { + tokensIn: sdk.NewCoins( + sdk.NewInt64Coin("foo", 1), + sdk.NewInt64Coin("bar", 1), + ), + poolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 10_000_000_000), + sdk.NewInt64Coin("bar", 10_000_000_000), + ), + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expNumShare: sdk.ZeroInt(), + expTokensJoined: sdk.Coins{}, + expPoolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 10_000_000_000), + sdk.NewInt64Coin("bar", 10_000_000_000), + ), + expectPass: false, + }, + "single-asset pool join exceeds hits max scaled asset amount": { + tokensIn: sdk.NewCoins( + sdk.NewInt64Coin("foo", 1), + ), + poolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 10_000_000_000), + sdk.NewInt64Coin("bar", 10_000_000_000), + ), + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expNumShare: sdk.ZeroInt(), + expTokensJoined: sdk.Coins{}, + expPoolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 10_000_000_000), + sdk.NewInt64Coin("bar", 10_000_000_000), + ), + expectPass: false, + }, + "all-asset pool join attempt exactly hits max scaled asset amount": { + tokensIn: sdk.NewCoins( + sdk.NewInt64Coin("foo", 1), + sdk.NewInt64Coin("bar", 1), + ), + poolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 9_999_999_999), + sdk.NewInt64Coin("bar", 9_999_999_999), + ), + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expNumShare: sdk.NewInt(10000000000), + expTokensJoined: sdk.NewCoins( + sdk.NewInt64Coin("foo", 1), + sdk.NewInt64Coin("bar", 1), + ), + expPoolAssets: sdk.NewCoins( + sdk.NewInt64Coin("foo", 10_000_000_000), + sdk.NewInt64Coin("bar", 10_000_000_000), + ), + expectPass: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := sdk.Context{} + p := poolStructFromAssets(tc.poolAssets, tc.scalingFactors) + + shares, joinedLiquidity, err := p.joinPoolSharesInternal(ctx, tc.tokensIn, tc.swapFee) + + if tc.expectPass { + require.Equal(t, tc.expNumShare, shares) + require.Equal(t, tc.expTokensJoined, joinedLiquidity) + require.Equal(t, tc.expPoolAssets, p.PoolLiquidity) + } + osmoassert.ConditionalError(t, !tc.expectPass, err) + }) + } +} From 2b610d1a518b285fa56ba3716efc30a134570541 Mon Sep 17 00:00:00 2001 From: alpo Date: Sat, 22 Oct 2022 20:26:33 -0700 Subject: [PATCH 6/9] fix single join bug, remove uneven ratio joins, and add inverse join tests --- x/gamm/pool-models/stableswap/amm.go | 16 +-- x/gamm/pool-models/stableswap/amm_test.go | 6 +- x/gamm/pool-models/stableswap/pool_test.go | 153 +++++++++++++++++++++ 3 files changed, 158 insertions(+), 17 deletions(-) diff --git a/x/gamm/pool-models/stableswap/amm.go b/x/gamm/pool-models/stableswap/amm.go index 49d9c14bbbb..31518d4d6d8 100644 --- a/x/gamm/pool-models/stableswap/amm.go +++ b/x/gamm/pool-models/stableswap/amm.go @@ -326,6 +326,7 @@ func (p *Pool) joinPoolSharesInternal(ctx sdk.Context, tokensIn sdk.Coins, swapF if len(tokensIn) == 1 && tokensIn[0].Amount.GT(sdk.OneInt()) { numShares, err = p.calcSingleAssetJoinShares(tokensIn[0], swapFee) newLiquidity = tokensIn + p.updatePoolForJoin(newLiquidity, numShares) return numShares, newLiquidity, err } else if len(tokensIn) != p.NumAssets() { return sdk.ZeroInt(), sdk.NewCoins(), errors.New( @@ -339,20 +340,7 @@ func (p *Pool) joinPoolSharesInternal(ctx sdk.Context, tokensIn sdk.Coins, swapF } p.updatePoolForJoin(tokensIn.Sub(remCoins), numShares) - tokensJoined := tokensIn - for _, coin := range remCoins { - // TODO: Perhaps add a method to skip if this is too small. - if coin.Amount.GT(sdk.OneInt()) { - newShare, err := p.calcSingleAssetJoinShares(coin, swapFee) - if err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err - } - p.updatePoolForJoin(sdk.NewCoins(coin), newShare) - numShares = numShares.Add(newShare) - } else { - tokensJoined = tokensJoined.Sub(sdk.NewCoins(coin)) - } - } + tokensJoined := tokensIn.Sub(remCoins) if err = validatePoolAssets(p.PoolLiquidity, p.ScalingFactor); err != nil { return sdk.ZeroInt(), sdk.NewCoins(), err diff --git a/x/gamm/pool-models/stableswap/amm_test.go b/x/gamm/pool-models/stableswap/amm_test.go index 0695c96c30e..85d249d2ff1 100644 --- a/x/gamm/pool-models/stableswap/amm_test.go +++ b/x/gamm/pool-models/stableswap/amm_test.go @@ -754,9 +754,9 @@ func TestJoinPoolSharesInternal(t *testing.T) { poolAssets: twoEvenStablePoolAssets, scalingFactors: defaultTwoAssetScalingFactors, swapFee: sdk.ZeroDec(), - expNumShare: sdk.NewIntFromUint64(10000000500000000000), - expTokensJoined: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPoolRaw)), sdk.NewCoin("bar", sdk.NewInt(10+tenPercentOfTwoPoolRaw))), - expPoolAssets: twoAssetPlusTenPercent.Add(sdk.NewCoin("bar", sdk.NewInt(10))), + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPoolRaw)), sdk.NewCoin("bar", sdk.NewInt(tenPercentOfTwoPoolRaw))), + expPoolAssets: twoAssetPlusTenPercent, expectPass: true, }, "all-asset pool join attempt exceeds max scaled asset amount": { diff --git a/x/gamm/pool-models/stableswap/pool_test.go b/x/gamm/pool-models/stableswap/pool_test.go index 67e1d6f168e..2457298071c 100644 --- a/x/gamm/pool-models/stableswap/pool_test.go +++ b/x/gamm/pool-models/stableswap/pool_test.go @@ -10,6 +10,7 @@ import ( "github.com/osmosis-labs/osmosis/v12/app/apptesting/osmoassert" "github.com/osmosis-labs/osmosis/v12/osmomath" + "github.com/osmosis-labs/osmosis/v12/x/gamm/pool-models/internal/cfmm_common" "github.com/osmosis-labs/osmosis/v12/x/gamm/types" ) @@ -639,3 +640,155 @@ func TestSwapInAmtGivenOut(t *testing.T) { }) } } + +func TestInverseJoinPoolExitPool(t *testing.T) { + hundredFoo := sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(100))) + thousandAssetA := sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(1000))) + tenPercentOfTwoPoolRaw := int64(1000000000 / 10) + tenPercentOfTwoPoolCoins := sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(int64(1000000000/10))), sdk.NewCoin("bar", sdk.NewInt(int64(1000000000/10)))) + // twoAssetPlusTenPercent := twoEvenStablePoolAssets.Add(tenPercentOfTwoPoolCoins...) + type testcase struct { + tokensIn sdk.Coins + poolAssets sdk.Coins + unevenJoinedTokens sdk.Coins + scalingFactors []uint64 + swapFee sdk.Dec + expectPass bool + } + + tests := map[string]testcase{ + "[single asset join] even two asset pool, no swap fee": { + tokensIn: hundredFoo, + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expectPass: true, + }, + "[single asset join] uneven two asset pool, no swap fee": { + tokensIn: hundredFoo, + poolAssets: twoUnevenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expectPass: true, + }, + "[single asset join] even 3-asset pool, no swap fee": { + tokensIn: thousandAssetA, + poolAssets: threeEvenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expectPass: true, + }, + "[single asset join] uneven 3-asset pool, no swap fee": { + tokensIn: thousandAssetA, + poolAssets: threeUnevenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expectPass: true, + }, + "[single asset join] even two asset pool, default swap fee": { + tokensIn: hundredFoo, + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: defaultSwapFee, + expectPass: true, + }, + "[single asset join] uneven two asset pool, default swap fee": { + tokensIn: hundredFoo, + poolAssets: twoUnevenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: defaultSwapFee, + expectPass: true, + }, + "[single asset join] even 3-asset pool, default swap fee": { + tokensIn: thousandAssetA, + poolAssets: threeEvenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + swapFee: defaultSwapFee, + expectPass: true, + }, + "[single asset join] uneven 3-asset pool, default swap fee": { + tokensIn: thousandAssetA, + poolAssets: threeUnevenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + swapFee: defaultSwapFee, + expectPass: true, + }, + "[single asset join] even 3-asset pool, 0.03 swap fee": { + tokensIn: thousandAssetA, + poolAssets: threeEvenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + swapFee: sdk.MustNewDecFromStr("0.03"), + expectPass: true, + }, + "[single asset join] uneven 3-asset pool, 0.03 swap fee": { + tokensIn: thousandAssetA, + poolAssets: threeUnevenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + swapFee: sdk.MustNewDecFromStr("0.03"), + expectPass: true, + }, + + "[all asset join] even two asset pool, same tokenIn ratio": { + tokensIn: tenPercentOfTwoPoolCoins, + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expectPass: true, + }, + "[all asset join] even two asset pool, different tokenIn ratio with pool": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPoolRaw)), sdk.NewCoin("bar", sdk.NewInt(10+tenPercentOfTwoPoolRaw))), + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expectPass: true, + }, + "[all asset join] even two asset pool, different tokenIn ratio with pool, nonzero swap fee": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPoolRaw)), sdk.NewCoin("bar", sdk.NewInt(10+tenPercentOfTwoPoolRaw))), + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expectPass: true, + }, + "[all asset join] even two asset pool, no tokens in": { + tokensIn: sdk.NewCoins(), + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + swapFee: sdk.ZeroDec(), + expectPass: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := sdk.Context{} + p := poolStructFromAssets(tc.poolAssets, tc.scalingFactors) + + // we join then exit the pool + shares, err := p.JoinPool(ctx, tc.tokensIn, tc.swapFee) + tokenOut, err := p.ExitPool(ctx, shares, defaultExitFee) + + // if single asset join, we swap output tokens to input denom to test the full inverse relationship + if len(tc.tokensIn) == 1 { + tokenOutAmt, err := cfmm_common.SwapAllCoinsToSingleAsset(&p, ctx, tokenOut, tc.tokensIn[0].Denom) + require.NoError(t, err) + tokenOut = sdk.NewCoins(sdk.NewCoin(tc.tokensIn[0].Denom, tokenOutAmt)) + } + + // if single asset join, we expect output token swapped into the input denom to be input minus swap fee + var expectedTokenOut sdk.Coins + if len(tc.tokensIn) == 1 { + expectedAmt := (tc.tokensIn[0].Amount.ToDec().Mul(sdk.OneDec().Sub(tc.swapFee))).TruncateInt() + expectedTokenOut = sdk.NewCoins(sdk.NewCoin(tc.tokensIn[0].Denom, expectedAmt)) + } else { + expectedTokenOut = tc.tokensIn + } + + if tc.expectPass { + finalPoolLiquidity := p.GetTotalPoolLiquidity(ctx) + require.True(t, tokenOut.IsAllLTE(expectedTokenOut)) + require.True(t, finalPoolLiquidity.IsAllGTE(tc.poolAssets)) + } + osmoassert.ConditionalError(t, !tc.expectPass, err) + }) + } +} From aa23808168c2e6ae044fab862dcc7dd996f95b47 Mon Sep 17 00:00:00 2001 From: alpo Date: Sat, 22 Oct 2022 20:36:29 -0700 Subject: [PATCH 7/9] add error checks to single asset joins --- x/gamm/pool-models/stableswap/amm.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/x/gamm/pool-models/stableswap/amm.go b/x/gamm/pool-models/stableswap/amm.go index 31518d4d6d8..ef6605dee0e 100644 --- a/x/gamm/pool-models/stableswap/amm.go +++ b/x/gamm/pool-models/stableswap/amm.go @@ -325,8 +325,18 @@ func (p *Pool) joinPoolSharesInternal(ctx sdk.Context, tokensIn sdk.Coins, swapF } if len(tokensIn) == 1 && tokensIn[0].Amount.GT(sdk.OneInt()) { numShares, err = p.calcSingleAssetJoinShares(tokensIn[0], swapFee) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + newLiquidity = tokensIn + p.updatePoolForJoin(newLiquidity, numShares) + + if err = validatePoolAssets(p.PoolLiquidity, p.ScalingFactor); err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + return numShares, newLiquidity, err } else if len(tokensIn) != p.NumAssets() { return sdk.ZeroInt(), sdk.NewCoins(), errors.New( From f213ed385921406271c846b45480547c91e6a604 Mon Sep 17 00:00:00 2001 From: alpo Date: Sun, 23 Oct 2022 16:40:18 -0700 Subject: [PATCH 8/9] fix mistake in test case --- x/gamm/pool-models/stableswap/pool_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/gamm/pool-models/stableswap/pool_test.go b/x/gamm/pool-models/stableswap/pool_test.go index 2457298071c..1fffd3c51a3 100644 --- a/x/gamm/pool-models/stableswap/pool_test.go +++ b/x/gamm/pool-models/stableswap/pool_test.go @@ -746,7 +746,7 @@ func TestInverseJoinPoolExitPool(t *testing.T) { tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPoolRaw)), sdk.NewCoin("bar", sdk.NewInt(10+tenPercentOfTwoPoolRaw))), poolAssets: twoEvenStablePoolAssets, scalingFactors: defaultTwoAssetScalingFactors, - swapFee: sdk.ZeroDec(), + swapFee: defaultSwapFee, expectPass: true, }, "[all asset join] even two asset pool, no tokens in": { From 68025d659ba03901d15c2c427d3181cdb26a39a6 Mon Sep 17 00:00:00 2001 From: alpo Date: Mon, 24 Oct 2022 10:29:25 -0700 Subject: [PATCH 9/9] remove commented line --- x/gamm/pool-models/stableswap/pool_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/x/gamm/pool-models/stableswap/pool_test.go b/x/gamm/pool-models/stableswap/pool_test.go index 1fffd3c51a3..1e61e834861 100644 --- a/x/gamm/pool-models/stableswap/pool_test.go +++ b/x/gamm/pool-models/stableswap/pool_test.go @@ -646,7 +646,6 @@ func TestInverseJoinPoolExitPool(t *testing.T) { thousandAssetA := sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(1000))) tenPercentOfTwoPoolRaw := int64(1000000000 / 10) tenPercentOfTwoPoolCoins := sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(int64(1000000000/10))), sdk.NewCoin("bar", sdk.NewInt(int64(1000000000/10)))) - // twoAssetPlusTenPercent := twoEvenStablePoolAssets.Add(tenPercentOfTwoPoolCoins...) type testcase struct { tokensIn sdk.Coins poolAssets sdk.Coins