diff --git a/x/gamm/keeper/pool_test.go b/x/gamm/keeper/pool_test.go index 3bcd9f2a3cd..22d99dabfdb 100644 --- a/x/gamm/keeper/pool_test.go +++ b/x/gamm/keeper/pool_test.go @@ -282,7 +282,8 @@ func (suite *KeeperTestSuite) TestGetPoolAndPoke() { }, sdk.NewCoins(sdk.NewCoin(defaultAcctFunds[0].Denom, defaultAcctFunds[0].Amount.QuoRaw(2)), sdk.NewCoin(defaultAcctFunds[1].Denom, defaultAcctFunds[1].Amount.QuoRaw(2))), []uint64{1, 1}, - )}, + ), + }, } for name, tc := range tests { diff --git a/x/gamm/pool-models/stableswap/pool.go b/x/gamm/pool-models/stableswap/pool.go index 29418f546a6..114fc129bc3 100644 --- a/x/gamm/pool-models/stableswap/pool.go +++ b/x/gamm/pool-models/stableswap/pool.go @@ -297,19 +297,48 @@ func (p *Pool) CalcJoinPoolShares(ctx sdk.Context, tokensIn sdk.Coins, swapFee s return pCopy.joinPoolSharesInternal(ctx, tokensIn, swapFee) } -// TODO: implement this -func (p *Pool) CalcJoinPoolNoSwapShares(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, newLiquidity sdk.Coins, err error) { - return sdk.ZeroInt(), nil, err +// CalcJoinPoolNoSwapShares calculates the number of shares created to execute an all-asset pool join with the provided amount of `tokensIn`. +// The input tokens must contain the same tokens as in the pool. +// +// Returns the number of shares created, the amount of coins actually joined into the pool as not all may tokens may be joinable. +// If an all-asset join is not possible, returns an error. +func (p Pool) CalcJoinPoolNoSwapShares(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, tokensJoined sdk.Coins, err error) { + // ensure that there aren't too many or too few assets in `tokensIn` + if tokensIn.Len() != p.NumAssets() || !tokensIn.DenomsSubsetOf(p.GetTotalPoolLiquidity(ctx)) { + return sdk.ZeroInt(), sdk.NewCoins(), errors.New("no-swap joins require LP'ing with all assets in pool") + } + + // execute a no-swap join with as many tokens as possible given a perfect ratio: + // * numShares is how many shares are perfectly matched. + // * remainingTokensIn is how many coins we have left to join that have not already been used. + numShares, remainingTokensIn, err := cfmm_common.MaximalExactRatioJoin(&p, ctx, tokensIn) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + + // ensure that no more tokens have been joined than is possible with the given `tokensIn` + tokensJoined = tokensIn.Sub(remainingTokensIn) + if tokensJoined.IsAnyGT(tokensIn) { + return sdk.ZeroInt(), sdk.NewCoins(), errors.New("an error has occurred, more coins joined than token In") + } + + return numShares, tokensJoined, nil } -func (p *Pool) JoinPool(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, err error) { - numShares, _, err = p.joinPoolSharesInternal(ctx, tokensIn, swapFee) +func (p *Pool) JoinPool(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (sdk.Int, error) { + numShares, _, err := p.joinPoolSharesInternal(ctx, tokensIn, swapFee) return numShares, err } -// TODO: implement this -func (p *Pool) JoinPoolNoSwap(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, err error) { - return sdk.ZeroInt(), err +func (p *Pool) JoinPoolNoSwap(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (sdk.Int, error) { + newShares, tokensJoined, err := p.CalcJoinPoolNoSwapShares(ctx, tokensIn, swapFee) + if err != nil { + return sdk.Int{}, err + } + + // update pool with the calculated share and liquidity needed to join pool + p.updatePoolForJoin(tokensJoined, newShares) + return newShares, nil } func (p *Pool) ExitPool(ctx sdk.Context, exitingShares sdk.Int, exitFee sdk.Dec) (exitingCoins sdk.Coins, err error) { diff --git a/x/gamm/pool-models/stableswap/pool_test.go b/x/gamm/pool-models/stableswap/pool_test.go index 1e61e834861..c408b9b180d 100644 --- a/x/gamm/pool-models/stableswap/pool_test.go +++ b/x/gamm/pool-models/stableswap/pool_test.go @@ -507,6 +507,178 @@ func TestScaleCoin(t *testing.T) { } } +func TestCalcJoinPoolNoSwapShares(t *testing.T) { + tenPercentOfTwoPool := int64(1000000000 / 10) + tenPercentOfThreePool := int64(1000000 / 10) + tests := map[string]struct { + tokensIn sdk.Coins + poolAssets sdk.Coins + scalingFactors []uint64 + expNumShare sdk.Int + expTokensJoined sdk.Coins + expPoolAssets sdk.Coins + expectPass bool + }{ + "even two asset pool, same tokenIn ratio": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPool)), sdk.NewCoin("bar", sdk.NewInt(tenPercentOfTwoPool))), + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPool)), sdk.NewCoin("bar", sdk.NewInt(tenPercentOfTwoPool))), + expPoolAssets: twoEvenStablePoolAssets, + expectPass: true, + }, + "even two asset pool, different tokenIn ratio with pool": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPool)), sdk.NewCoin("bar", sdk.NewInt(tenPercentOfTwoPool+1))), + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPool)), sdk.NewCoin("bar", sdk.NewInt(tenPercentOfTwoPool))), + expPoolAssets: twoEvenStablePoolAssets, + expectPass: true, + }, + "uneven two asset pool, same tokenIn ratio": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(2*tenPercentOfTwoPool)), sdk.NewCoin("bar", sdk.NewInt(tenPercentOfTwoPool))), + poolAssets: twoUnevenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(2*tenPercentOfTwoPool)), sdk.NewCoin("bar", sdk.NewInt(tenPercentOfTwoPool))), + expPoolAssets: twoUnevenStablePoolAssets, + expectPass: true, + }, + "uneven two asset pool, different tokenIn ratio with pool": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(2*tenPercentOfTwoPool)), sdk.NewCoin("bar", sdk.NewInt(tenPercentOfTwoPool+1))), + poolAssets: twoUnevenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(2*tenPercentOfTwoPool)), sdk.NewCoin("bar", sdk.NewInt(tenPercentOfTwoPool))), + expPoolAssets: twoUnevenStablePoolAssets, + expectPass: true, + }, + "even three asset pool, same tokenIn ratio": { + tokensIn: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(tenPercentOfThreePool))), + poolAssets: threeEvenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(tenPercentOfThreePool))), + expPoolAssets: threeEvenStablePoolAssets, + expectPass: true, + }, + "even three asset pool, different tokenIn ratio with pool": { + tokensIn: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(tenPercentOfThreePool+1))), + poolAssets: threeEvenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(tenPercentOfThreePool))), + expPoolAssets: threeEvenStablePoolAssets, + expectPass: true, + }, + "uneven three asset pool, same tokenIn ratio": { + tokensIn: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(2*tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(3*tenPercentOfThreePool))), + poolAssets: threeUnevenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(2*tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(3*tenPercentOfThreePool))), + expPoolAssets: threeUnevenStablePoolAssets, + expectPass: true, + }, + "uneven three asset pool, different tokenIn ratio with pool": { + tokensIn: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(2*tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(3*tenPercentOfThreePool+1))), + poolAssets: threeUnevenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(2*tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(3*tenPercentOfThreePool))), + expPoolAssets: threeUnevenStablePoolAssets, + expectPass: true, + }, + "uneven three asset pool, uneven scaling factors": { + tokensIn: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(2*tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(3*tenPercentOfThreePool))), + poolAssets: threeUnevenStablePoolAssets, + scalingFactors: []uint64{5, 9, 175}, + expNumShare: sdk.NewIntFromUint64(10000000000000000000), + expTokensJoined: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(2*tenPercentOfThreePool)), sdk.NewCoin("asset/c", sdk.NewInt(3*tenPercentOfThreePool))), + expPoolAssets: threeUnevenStablePoolAssets, + expectPass: true, + }, + + // error catching + "even two asset pool, no-swap join attempt with one asset": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPool))), + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(0), + expTokensJoined: sdk.Coins{}, + expPoolAssets: twoEvenStablePoolAssets, + expectPass: false, + }, + "even two asset pool, no-swap join attempt with one valid and one invalid asset": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(tenPercentOfTwoPool)), sdk.NewCoin("baz", sdk.NewInt(tenPercentOfTwoPool))), + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(0), + expTokensJoined: sdk.Coins{}, + expPoolAssets: twoEvenStablePoolAssets, + expectPass: false, + }, + "even two asset pool, no-swap join attempt with two invalid assets": { + tokensIn: sdk.NewCoins(sdk.NewCoin("baz", sdk.NewInt(tenPercentOfTwoPool)), sdk.NewCoin("qux", sdk.NewInt(tenPercentOfTwoPool))), + poolAssets: twoEvenStablePoolAssets, + scalingFactors: defaultTwoAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(0), + expTokensJoined: sdk.Coins{}, + expPoolAssets: twoEvenStablePoolAssets, + expectPass: false, + }, + "even three asset pool, no-swap join attempt with an invalid asset": { + tokensIn: sdk.NewCoins(sdk.NewCoin("asset/a", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("asset/b", sdk.NewInt(tenPercentOfThreePool)), sdk.NewCoin("qux", sdk.NewInt(tenPercentOfThreePool))), + poolAssets: threeEvenStablePoolAssets, + scalingFactors: defaultThreeAssetScalingFactors, + expNumShare: sdk.NewIntFromUint64(0), + expTokensJoined: sdk.Coins{}, + expPoolAssets: threeEvenStablePoolAssets, + expectPass: false, + }, + "single asset pool, no-swap join attempt with one asset": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(sdk.MaxSortableDec.TruncateInt64()))), + poolAssets: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1))), + scalingFactors: []uint64{1}, + expNumShare: sdk.NewIntFromUint64(0), + expTokensJoined: sdk.Coins{}, + expPoolAssets: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1))), + expectPass: false, + }, + "attempt joining pool with no assets in it": { + tokensIn: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1))), + poolAssets: sdk.Coins{}, + scalingFactors: []uint64{}, + expNumShare: sdk.NewIntFromUint64(0), + expTokensJoined: sdk.Coins{}, + expPoolAssets: sdk.Coins{}, + expectPass: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctx := sdk.Context{} + pool := poolStructFromAssets(test.poolAssets, test.scalingFactors) + numShare, tokensJoined, err := pool.CalcJoinPoolNoSwapShares(ctx, test.tokensIn, pool.GetSwapFee(ctx)) + + if test.expectPass { + require.NoError(t, err) + require.Equal(t, test.expPoolAssets, pool.GetTotalPoolLiquidity(ctx)) + require.Equal(t, test.expNumShare, numShare) + require.Equal(t, test.expTokensJoined, tokensJoined) + } else { + require.Error(t, err) + require.Equal(t, test.expPoolAssets, pool.GetTotalPoolLiquidity(ctx)) + require.Equal(t, test.expNumShare, numShare) + require.Equal(t, test.expTokensJoined, tokensJoined) + } + }) + } +} + func TestSwapOutAmtGivenIn(t *testing.T) { tests := map[string]struct { poolAssets sdk.Coins