From 286b5941c9c4a35cadbcfffa6fc67af8d0b859ab Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 17 Jul 2022 22:35:44 -0500 Subject: [PATCH] refactor: consolidate pool implementation (backport #1868) (#2111) * refactor: consolidate pool implementation (#1868) (cherry picked from commit e2d49117ea16b41cdf17e23aee590e60d384dc32) # Conflicts: # x/gamm/pool-models/balancer/amm.go # x/gamm/pool-models/balancer/amm_test.go # x/gamm/pool-models/balancer/balancer_pool.go # x/gamm/pool-models/balancer/pool_suite_test.go # x/gamm/pool-models/balancer/suite_test.go * Add import fixes * Fix most merge conflicts (one remaining) * Fix remaining merge conflict Co-authored-by: Aleksandr Bezobchuk Co-authored-by: Dev Ojha --- x/gamm/pool-models/balancer/amm.go | 556 +------- x/gamm/pool-models/balancer/amm_test.go | 352 +---- x/gamm/pool-models/balancer/balancer_pool.go | 601 -------- .../balancer/balancer_pool_test.go | 567 -------- x/gamm/pool-models/balancer/marshal_test.go | 16 +- x/gamm/pool-models/balancer/pool.go | 949 +++++++++++++ x/gamm/pool-models/balancer/pool_params.go | 78 ++ ...mm_joinpool_test.go => pool_suite_test.go} | 608 ++------ x/gamm/pool-models/balancer/pool_test.go | 1243 +++++++++++++++++ x/gamm/pool-models/balancer/suite_test.go | 28 - x/gamm/pool-models/balancer/util_test.go | 50 + 11 files changed, 2545 insertions(+), 2503 deletions(-) delete mode 100644 x/gamm/pool-models/balancer/balancer_pool.go delete mode 100644 x/gamm/pool-models/balancer/balancer_pool_test.go create mode 100644 x/gamm/pool-models/balancer/pool.go create mode 100644 x/gamm/pool-models/balancer/pool_params.go rename x/gamm/pool-models/balancer/{amm_joinpool_test.go => pool_suite_test.go} (63%) create mode 100644 x/gamm/pool-models/balancer/pool_test.go delete mode 100644 x/gamm/pool-models/balancer/suite_test.go diff --git a/x/gamm/pool-models/balancer/amm.go b/x/gamm/pool-models/balancer/amm.go index 91c3d0413c2..eaa7c69d19d 100644 --- a/x/gamm/pool-models/balancer/amm.go +++ b/x/gamm/pool-models/balancer/amm.go @@ -1,26 +1,85 @@ package balancer import ( - "errors" "fmt" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/osmosis-labs/osmosis/v10/osmomath" - "github.com/osmosis-labs/osmosis/v10/x/gamm/pool-models/internal/cfmm_common" "github.com/osmosis-labs/osmosis/v10/x/gamm/types" ) -const ( - errMsgFormatSharesAmountNotPositive = "shares amount must be positive, was %d" - errMsgFormatTokenAmountNotPositive = "token amount must be positive, was %d" - errMsgFormatTokensLargerThanMax = "%d resulted tokens is larger than the max amount of %d" - errMsgFormatSharesLargerThanMax = "%d resulted shares is larger than the max amount of %d" - errMsgFormatFailedInterimLiquidityUpdate = "failed to update interim liquidity - pool asset %s does not exist" - errMsgFormatRepeatingPoolAssetsNotAllowed = "repeating pool assets not allowed, found %s" - v10Fork = 4713065 -) +// subPoolAssetWeights subtracts the weights of two different pool asset slices. +// It assumes that both pool assets have the same token denominations, +// with the denominations in the same order. +// Returned weights can (and probably will have some) be negative. +func subPoolAssetWeights(base []PoolAsset, other []PoolAsset) []PoolAsset { + weightDifference := make([]PoolAsset, len(base)) + // TODO: Consider deleting these panics for performance + if len(base) != len(other) { + panic("subPoolAssetWeights called with invalid input, len(base) != len(other)") + } + for i, asset := range base { + if asset.Token.Denom != other[i].Token.Denom { + panic(fmt.Sprintf("subPoolAssetWeights called with invalid input, "+ + "expected other's %vth asset to be %v, got %v", + i, asset.Token.Denom, other[i].Token.Denom)) + } + curWeightDiff := asset.Weight.Sub(other[i].Weight) + weightDifference[i] = PoolAsset{Token: asset.Token, Weight: curWeightDiff} + } + return weightDifference +} + +// addPoolAssetWeights adds the weights of two different pool asset slices. +// It assumes that both pool assets have the same token denominations, +// with the denominations in the same order. +// Returned weights can be negative. +func addPoolAssetWeights(base []PoolAsset, other []PoolAsset) []PoolAsset { + weightSum := make([]PoolAsset, len(base)) + // TODO: Consider deleting these panics for performance + if len(base) != len(other) { + panic("addPoolAssetWeights called with invalid input, len(base) != len(other)") + } + for i, asset := range base { + if asset.Token.Denom != other[i].Token.Denom { + panic(fmt.Sprintf("addPoolAssetWeights called with invalid input, "+ + "expected other's %vth asset to be %v, got %v", + i, asset.Token.Denom, other[i].Token.Denom)) + } + curWeightSum := asset.Weight.Add(other[i].Weight) + weightSum[i] = PoolAsset{Token: asset.Token, Weight: curWeightSum} + } + return weightSum +} + +// assumes 0 < d < 1 +func poolAssetsMulDec(base []PoolAsset, d sdk.Dec) []PoolAsset { + newWeights := make([]PoolAsset, len(base)) + for i, asset := range base { + // TODO: This can adversarially panic at the moment! (as can Pool.TotalWeight) + // Ensure this won't be able to panic in the future PR where we bound + // each assets weight, and add precision + newWeight := d.MulInt(asset.Weight).RoundInt() + newWeights[i] = PoolAsset{Token: asset.Token, Weight: newWeight} + } + return newWeights +} + +// ValidateUserSpecifiedWeight ensures that a weight that is provided from user-input anywhere +// for creating a pool obeys the expected guarantees. +// Namely, that the weight is in the range [1, MaxUserSpecifiedWeight) +func ValidateUserSpecifiedWeight(weight sdk.Int) error { + if !weight.IsPositive() { + return sdkerrors.Wrap(types.ErrNotPositiveWeight, weight.String()) + } + + if weight.GTE(MaxUserSpecifiedWeight) { + return sdkerrors.Wrap(types.ErrWeightTooLarge, weight.String()) + } + return nil +} // solveConstantFunctionInvariant solves the constant function of an AMM // that determines the relationship between the differences of two sides @@ -52,158 +111,6 @@ func solveConstantFunctionInvariant( return amountY } -// CalcOutAmtGivenIn calculates tokens to be swapped out given the provided -// amount and fee deducted, using solveConstantFunctionInvariant. -func (p Pool) CalcOutAmtGivenIn( - ctx sdk.Context, - tokensIn sdk.Coins, - tokenOutDenom string, - swapFee sdk.Dec, -) (sdk.Coin, error) { - tokenIn, poolAssetIn, poolAssetOut, err := p.parsePoolAssets(tokensIn, tokenOutDenom) - if err != nil { - return sdk.Coin{}, err - } - - tokenAmountInAfterFee := tokenIn.Amount.ToDec().Mul(sdk.OneDec().Sub(swapFee)) - poolTokenInBalance := poolAssetIn.Token.Amount.ToDec() - poolPostSwapInBalance := poolTokenInBalance.Add(tokenAmountInAfterFee) - - // deduct swapfee on the tokensIn - // delta balanceOut is positive(tokens inside the pool decreases) - tokenAmountOut := solveConstantFunctionInvariant( - poolTokenInBalance, - poolPostSwapInBalance, - poolAssetIn.Weight.ToDec(), - poolAssetOut.Token.Amount.ToDec(), - poolAssetOut.Weight.ToDec(), - ) - - // We ignore the decimal component, as we round down the token amount out. - tokenAmountOutInt := tokenAmountOut.TruncateInt() - if !tokenAmountOutInt.IsPositive() { - return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") - } - - return sdk.NewCoin(tokenOutDenom, tokenAmountOutInt), nil -} - -// SwapOutAmtGivenIn is a mutative method for CalcOutAmtGivenIn, which includes the actual swap. -func (p *Pool) SwapOutAmtGivenIn( - ctx sdk.Context, - tokensIn sdk.Coins, - tokenOutDenom string, - swapFee sdk.Dec, -) ( - tokenOut sdk.Coin, err error, -) { - tokenOutCoin, err := p.CalcOutAmtGivenIn(ctx, tokensIn, tokenOutDenom, swapFee) - if err != nil { - return sdk.Coin{}, err - } - - err = p.applySwap(ctx, tokensIn, sdk.Coins{tokenOutCoin}) - if err != nil { - return sdk.Coin{}, err - } - return tokenOutCoin, nil -} - -// CalcInAmtGivenOut calculates token to be provided, fee added, -// given the swapped out amount, using solveConstantFunctionInvariant. -func (p Pool) CalcInAmtGivenOut( - ctx sdk.Context, tokensOut sdk.Coins, tokenInDenom string, swapFee sdk.Dec) ( - tokenIn sdk.Coin, err error, -) { - tokenOut, poolAssetOut, poolAssetIn, err := p.parsePoolAssets(tokensOut, tokenInDenom) - if err != nil { - return sdk.Coin{}, err - } - - // delta balanceOut is positive(tokens inside the pool decreases) - poolTokenOutBalance := poolAssetOut.Token.Amount.ToDec() - poolPostSwapOutBalance := poolTokenOutBalance.Sub(tokenOut.Amount.ToDec()) - // (x_0)(y_0) = (x_0 + in)(y_0 - out) - tokenAmountIn := solveConstantFunctionInvariant( - poolTokenOutBalance, poolPostSwapOutBalance, poolAssetOut.Weight.ToDec(), - poolAssetIn.Token.Amount.ToDec(), poolAssetIn.Weight.ToDec()).Neg() - - // We deduct a swap fee on the input asset. The swap happens by following the invariant curve on the input * (1 - swap fee) - // and then the swap fee is added to the pool. - // Thus in order to give X amount out, we solve the invariant for the invariant input. However invariant input = (1 - swapfee) * trade input. - // Therefore we divide by (1 - swapfee) here - tokenAmountInBeforeFee := tokenAmountIn.Quo(sdk.OneDec().Sub(swapFee)) - - // We round up tokenInAmt, as this is whats charged for the swap, for the precise amount out. - // Otherwise, the pool would under-charge by this rounding error. - tokenInAmt := tokenAmountInBeforeFee.Ceil().TruncateInt() - - if !tokenInAmt.IsPositive() { - return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") - } - return sdk.NewCoin(tokenInDenom, tokenInAmt), nil -} - -// SwapInAmtGivenOut is a mutative method for CalcOutAmtGivenIn, which includes the actual swap. -func (p *Pool) SwapInAmtGivenOut( - ctx sdk.Context, tokensOut sdk.Coins, tokenInDenom string, swapFee sdk.Dec) ( - tokenIn sdk.Coin, err error, -) { - tokenInCoin, err := p.CalcInAmtGivenOut(ctx, tokensOut, tokenInDenom, swapFee) - if err != nil { - return sdk.Coin{}, err - } - - err = p.applySwap(ctx, sdk.Coins{tokenInCoin}, tokensOut) - if err != nil { - return sdk.Coin{}, err - } - return tokenInCoin, nil -} - -// ApplySwap. -func (p *Pool) applySwap(ctx sdk.Context, tokensIn sdk.Coins, tokensOut sdk.Coins) error { - // Also ensures that len(tokensIn) = 1 = len(tokensOut) - inPoolAsset, outPoolAsset, err := p.parsePoolAssetsCoins(tokensIn, tokensOut) - if err != nil { - return err - } - inPoolAsset.Token.Amount = inPoolAsset.Token.Amount.Add(tokensIn[0].Amount) - outPoolAsset.Token.Amount = outPoolAsset.Token.Amount.Sub(tokensOut[0].Amount) - - return p.UpdatePoolAssetBalances(sdk.NewCoins( - inPoolAsset.Token, - outPoolAsset.Token, - )) -} - -// SpotPrice returns the spot price of the pool -// This is the weight-adjusted balance of the tokens in the pool. -// In order reduce the propagated effect of incorrect trailing digits, -// we take the ratio of weights and divide this by ratio of supplies -// this is equivalent to spot_price = (Base_supply / Weight_base) / (Quote_supply / Weight_quote) -// but cancels out the common term in weight. -// -// panics if pool is misconfigured and has any weight as 0. -func (p Pool) SpotPrice(ctx sdk.Context, baseAsset, quoteAsset string) (sdk.Dec, error) { - quote, base, err := p.parsePoolAssetsByDenoms(quoteAsset, baseAsset) - if err != nil { - return sdk.Dec{}, err - } - if base.Weight.IsZero() || quote.Weight.IsZero() { - return sdk.Dec{}, errors.New("pool is misconfigured, got 0 weight") - } - - // spot_price = (Base_supply / Weight_base) / (Quote_supply / Weight_quote) - // spot_price = (weight_quote / weight_base) * (base_supply / quote_supply) - invWeightRatio := quote.Weight.ToDec().Quo(base.Weight.ToDec()) - supplyRatio := base.Token.Amount.ToDec().Quo(quote.Token.Amount.ToDec()) - fullRatio := supplyRatio.Mul(invWeightRatio) - // we want to round this to `SigFigs` of precision - ratio := osmomath.SigFigRound(fullRatio, types.SigFigs) - return ratio, nil -} - // balancer notation: pAo - pool shares amount out, given single asset in // the second argument requires the tokenWeightIn / total token weight. func calcPoolSharesOutGivenSingleAssetIn( @@ -236,178 +143,6 @@ func calcPoolSharesOutGivenSingleAssetIn( return poolAmountOut } -// calcPoolOutGivenSingleIn - balance pAo. -func (p *Pool) calcSingleAssetJoin(tokenIn sdk.Coin, swapFee sdk.Dec, tokenInPoolAsset PoolAsset, totalShares sdk.Int) (numShares sdk.Int, err error) { - _, err = p.GetPoolAsset(tokenIn.Denom) - if err != nil { - return sdk.ZeroInt(), err - } - - totalWeight := p.GetTotalWeight() - if totalWeight.IsZero() { - return sdk.ZeroInt(), errors.New("pool misconfigured, total weight = 0") - } - normalizedWeight := tokenInPoolAsset.Weight.ToDec().Quo(totalWeight.ToDec()) - return calcPoolSharesOutGivenSingleAssetIn( - tokenInPoolAsset.Token.Amount.ToDec(), - normalizedWeight, - totalShares.ToDec(), - tokenIn.Amount.ToDec(), - swapFee, - ).TruncateInt(), nil -} - -// JoinPool calculates the number of shares needed given tokensIn with swapFee applied. -// It updates the liquidity if the pool is joined successfully. If not, returns error. -// and updates pool accordingly. -func (p *Pool) JoinPool(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, err error) { - numShares, newLiquidity, err := p.CalcJoinPoolShares(ctx, tokensIn, swapFee) - if err != nil { - return sdk.Int{}, err - } - - // update pool with the calculated share and liquidity needed to join pool - p.IncreaseLiquidity(numShares, newLiquidity) - return numShares, nil -} - -func (p *Pool) calcJoinPoolSharesBroken(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, newLiquidity sdk.Coins, err error) { - poolAssets := p.GetAllPoolAssets() - poolAssetsByDenom := make(map[string]PoolAsset) - for _, poolAsset := range poolAssets { - poolAssetsByDenom[poolAsset.Token.Denom] = poolAsset - } - - totalShares := p.GetTotalShares() - - if tokensIn.Len() == 1 { - numShares, err = p.calcSingleAssetJoin(tokensIn[0], swapFee, poolAssetsByDenom[tokensIn[0].Denom], totalShares) - if err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err - } - - newLiquidity = tokensIn - - return numShares, newLiquidity, nil - } else if tokensIn.Len() != p.NumAssets() { - return sdk.ZeroInt(), sdk.NewCoins(), errors.New("balancer pool only supports LP'ing with one asset or all assets in pool") - } - - // Add all exact coins we can (no swap). ctx arg doesn't matter for Balancer. - numShares, remCoins, err := cfmm_common.MaximalExactRatioJoinBroken(p, sdk.Context{}, tokensIn) - if err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err - } - - // update liquidity for accurate calcSingleAssetJoin calculation - newLiquidity = tokensIn.Sub(remCoins) - for _, coin := range newLiquidity { - poolAsset := poolAssetsByDenom[coin.Denom] - poolAsset.Token.Amount = poolAssetsByDenom[coin.Denom].Token.Amount.Add(coin.Amount) - poolAssetsByDenom[coin.Denom] = poolAsset - } - - totalShares = totalShares.Add(numShares) - - // If there are coins that couldn't be perfectly joined, do single asset joins - // for each of them. - if !remCoins.Empty() { - for _, coin := range remCoins { - newShares, err := p.calcSingleAssetJoin(coin, swapFee, poolAssetsByDenom[coin.Denom], totalShares) - if err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err - } - - newLiquidity = newLiquidity.Add(coin) - numShares = numShares.Add(newShares) - } - } - - return numShares, newLiquidity, nil -} - -// CalcJoinPoolShares calculates the number of shares created to join pool with the provided amount of `tokenIn`. -// The input tokens must either be: -// - a single token -// - contain exactly the same tokens as the pool contains -// -// It returns the number of shares created, the amount of coins actually joined into the pool -// (in case of not being able to fully join), or an error. -func (p *Pool) CalcJoinPoolShares(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, tokensJoined sdk.Coins, err error) { - if ctx.BlockHeight() < v10Fork { - return p.calcJoinPoolSharesBroken(ctx, tokensIn, swapFee) - } - // 1) Get pool current liquidity + and token weights - // 2) If single token provided, do single asset join and exit. - // 3) If multi-asset join, first do as much of a join as we can with no swaps. - // 4) Update pool shares / liquidity / remaining tokens to join accordingly - // 5) For every remaining token to LP, do a single asset join, and update pool shares / liquidity. - // - // Note that all single asset joins do incur swap fee. - // - // Since CalcJoinPoolShares is non-mutative, the steps for updating pool shares / liquidity are - // more complex / don't just alter the state. - // We should simplify this logic further in the future, using balancer multi-join equations. - - // 1) get all 'pool assets' (aka current pool liquidity + balancer weight) - poolAssetsByDenom, err := getPoolAssetsByDenom(p.GetAllPoolAssets()) - if err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err - } - - totalShares := p.GetTotalShares() - if tokensIn.Len() == 1 { - // 2) Single token provided, so do single asset join and exit. - numShares, err = p.calcSingleAssetJoin(tokensIn[0], swapFee, poolAssetsByDenom[tokensIn[0].Denom], totalShares) - if err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err - } - // we join all the tokens. - tokensJoined = tokensIn - return numShares, tokensJoined, nil - } else if tokensIn.Len() != p.NumAssets() { - return sdk.ZeroInt(), sdk.NewCoins(), errors.New("balancer pool only supports LP'ing with one asset or all assets in pool") - } - - // 3) JoinPoolNoSwap with as many tokens as we can. (What is in 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. - // if remaining coins is empty, logic is done (we joined all tokensIn) - numShares, remainingTokensIn, err := cfmm_common.MaximalExactRatioJoin(p, sdk.Context{}, tokensIn) - if err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err - } - if remainingTokensIn.Empty() { - tokensJoined = tokensIn - return numShares, tokensJoined, nil - } - - // 4) Still more coins to join, so we update the effective pool state here to account for - // join that just happened. - // * We add the joined coins to our "current pool liquidity" object (poolAssetsByDenom) - // * We increment a variable for our "newTotalShares" to add in the shares that've been added. - tokensJoined = tokensIn.Sub(remainingTokensIn) - if err := updateIntermediaryPoolAssetsLiquidity(tokensJoined, poolAssetsByDenom); err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err - } - newTotalShares := totalShares.Add(numShares) - - // 5) Now single asset join each remaining coin. - newNumSharesFromRemaining, newLiquidityFromRemaining, err := p.calcJoinSingleAssetTokensIn(remainingTokensIn, newTotalShares, poolAssetsByDenom, swapFee) - if err != nil { - return sdk.ZeroInt(), sdk.NewCoins(), err - } - // update total amount LP'd variable, and total new LP shares variable, run safety check, and return - numShares = numShares.Add(newNumSharesFromRemaining) - tokensJoined = tokensJoined.Add(newLiquidityFromRemaining...) - - 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 -} - // getPoolAssetsByDenom return a mapping from pool asset // denom to the pool asset itself. There must be no duplicates. // Returns error, if any found. @@ -448,60 +183,6 @@ func updateIntermediaryPoolAssetsLiquidity(liquidity sdk.Coins, poolAssetsByDeno return nil } -// calcJoinSingleAssetTokensIn attempts to calculate single -// asset join for all tokensIn given totalShares in pool, -// poolAssetsByDenom and swapFee. totalShares is the number -// of shares in pool before beginnning to join any of the tokensIn. -// -// Returns totalNewShares and totalNewLiquidity from joining all tokensIn -// by mimicking individually single asset joining each. -// or error if fails to calculate join for any of the tokensIn. -func (p *Pool) calcJoinSingleAssetTokensIn(tokensIn sdk.Coins, totalShares sdk.Int, poolAssetsByDenom map[string]PoolAsset, swapFee sdk.Dec) (sdk.Int, sdk.Coins, error) { - totalNewShares := sdk.ZeroInt() - totalNewLiquidity := sdk.NewCoins() - for _, coin := range tokensIn { - newShares, err := p.calcSingleAssetJoin(coin, swapFee, poolAssetsByDenom[coin.Denom], totalShares.Add(totalNewShares)) - if err != nil { - return sdk.ZeroInt(), sdk.Coins{}, err - } - - totalNewLiquidity = totalNewLiquidity.Add(coin) - totalNewShares = totalNewShares.Add(newShares) - } - return totalNewShares, totalNewLiquidity, nil -} - -func (p *Pool) ExitPool(ctx sdk.Context, exitingShares sdk.Int, exitFee sdk.Dec) (exitingCoins sdk.Coins, err error) { - exitingCoins, err = p.CalcExitPoolShares(ctx, exitingShares, exitFee) - if err != nil { - return sdk.Coins{}, err - } - - if err := p.exitPool(ctx, exitingCoins, exitingShares); err != nil { - return sdk.Coins{}, err - } - - return exitingCoins, nil -} - -// exitPool exits the pool given exitingCoins and exitingShares. -// updates the pool's liquidity and totalShares. -func (p *Pool) exitPool(ctx sdk.Context, exitingCoins sdk.Coins, exitingShares sdk.Int) error { - balances := p.GetTotalPoolLiquidity(ctx).Sub(exitingCoins) - if err := p.UpdatePoolAssetBalances(balances); err != nil { - return err - } - - totalShares := p.GetTotalShares() - p.TotalShares = sdk.NewCoin(p.TotalShares.Denom, totalShares.Sub(exitingShares)) - - return nil -} - -func (p *Pool) CalcExitPoolShares(ctx sdk.Context, exitingShares sdk.Int, exitFee sdk.Dec) (exitedCoins sdk.Coins, err error) { - return cfmm_common.CalcExitPool(ctx, p, exitingShares, exitFee) -} - // feeRatio returns the fee ratio that is defined as follows: // 1 - ((1 - normalizedTokenWeightOut) * swapFee) func feeRatio(normalizedWeight, swapFee sdk.Dec) sdk.Dec { @@ -525,69 +206,6 @@ func calcSingleAssetInGivenPoolSharesOut( return tokenAmountInFeeIncluded } -func (p *Pool) CalcTokenInShareAmountOut( - ctx sdk.Context, - tokenInDenom string, - shareOutAmount sdk.Int, - swapFee sdk.Dec, -) (tokenInAmount sdk.Int, err error) { - _, poolAssetIn, err := p.getPoolAssetAndIndex(tokenInDenom) - if err != nil { - return sdk.Int{}, err - } - - normalizedWeight := poolAssetIn.Weight.ToDec().Quo(p.GetTotalWeight().ToDec()) - - // We round up tokenInAmount, as this is whats charged for the swap, for the precise amount out. - // Otherwise, the pool would under-charge by this rounding error. - tokenInAmount = calcSingleAssetInGivenPoolSharesOut( - poolAssetIn.Token.Amount.ToDec(), - normalizedWeight, - p.GetTotalShares().ToDec(), - shareOutAmount.ToDec(), - swapFee, - ).Ceil().TruncateInt() - - if !tokenInAmount.IsPositive() { - return sdk.Int{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, errMsgFormatTokenAmountNotPositive, tokenInAmount.Int64()) - } - - return tokenInAmount, nil -} - -func (p *Pool) JoinPoolTokenInMaxShareAmountOut( - ctx sdk.Context, - tokenInDenom string, - shareOutAmount sdk.Int, -) (tokenInAmount sdk.Int, err error) { - _, poolAssetIn, err := p.getPoolAssetAndIndex(tokenInDenom) - if err != nil { - return sdk.Int{}, err - } - - normalizedWeight := poolAssetIn.Weight.ToDec().Quo(p.GetTotalWeight().ToDec()) - - tokenInAmount = calcSingleAssetInGivenPoolSharesOut( - poolAssetIn.Token.Amount.ToDec(), - normalizedWeight, - p.GetTotalShares().ToDec(), - shareOutAmount.ToDec(), - p.GetSwapFee(ctx), - ).TruncateInt() - - if !tokenInAmount.IsPositive() { - return sdk.Int{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, errMsgFormatTokenAmountNotPositive, tokenInAmount.Int64()) - } - - poolAssetIn.Token.Amount = poolAssetIn.Token.Amount.Add(tokenInAmount) - err = p.UpdatePoolAssetBalance(poolAssetIn.Token) - if err != nil { - return sdk.Int{}, err - } - - return tokenInAmount, nil -} - // calcPoolSharesInGivenSingleAssetOut returns pool shares amount in, given single asset out. // the returned shares in have the fee included in them. // the second argument requires the tokenWeightOut / total token weight. @@ -610,37 +228,3 @@ func calcPoolSharesInGivenSingleAssetOut( sharesInFeeIncluded := sharesIn.Quo(sdk.OneDec().Sub(exitFee)) return sharesInFeeIncluded } - -func (p *Pool) ExitSwapExactAmountOut( - ctx sdk.Context, - tokenOut sdk.Coin, - shareInMaxAmount sdk.Int, -) (shareInAmount sdk.Int, err error) { - _, poolAssetOut, err := p.getPoolAssetAndIndex(tokenOut.Denom) - if err != nil { - return sdk.Int{}, err - } - - sharesIn := calcPoolSharesInGivenSingleAssetOut( - poolAssetOut.Token.Amount.ToDec(), - poolAssetOut.Weight.ToDec().Quo(p.TotalWeight.ToDec()), - p.GetTotalShares().ToDec(), - tokenOut.Amount.ToDec(), - p.GetSwapFee(ctx), - p.GetExitFee(ctx), - ).TruncateInt() - - if !sharesIn.IsPositive() { - return sdk.Int{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, errMsgFormatSharesAmountNotPositive, sharesIn.Int64()) - } - - if sharesIn.GT(shareInMaxAmount) { - return sdk.Int{}, sdkerrors.Wrapf(types.ErrLimitMaxAmount, errMsgFormatSharesLargerThanMax, sharesIn.Int64(), shareInMaxAmount.Uint64()) - } - - if err := p.exitPool(ctx, sdk.NewCoins(tokenOut), sharesIn); err != nil { - return sdk.Int{}, err - } - - return sharesIn, nil -} diff --git a/x/gamm/pool-models/balancer/amm_test.go b/x/gamm/pool-models/balancer/amm_test.go index 43a7eecbead..90edf0cd560 100644 --- a/x/gamm/pool-models/balancer/amm_test.go +++ b/x/gamm/pool-models/balancer/amm_test.go @@ -1,334 +1,52 @@ package balancer_test import ( - "fmt" "testing" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "github.com/osmosis-labs/osmosis/v10/osmoutils" "github.com/osmosis-labs/osmosis/v10/x/gamm/pool-models/balancer" - "github.com/osmosis-labs/osmosis/v10/x/gamm/types" ) -// This test sets up 2 asset pools, and then checks the spot price on them. -// It uses the pools spot price method, rather than the Gamm keepers spot price method. -func (suite *KeeperTestSuite) TestBalancerSpotPrice() { - baseDenom := "uosmo" - quoteDenom := "uion" - +func TestBalancerPoolParams(t *testing.T) { + // Tests that creating a pool with the given pair of swapfee and exit fee + // errors or succeeds as intended. Furthermore, it checks that + // NewPool panics in the error case. tests := []struct { - name string - baseDenomPoolInput sdk.Coin - quoteDenomPoolInput sdk.Coin - expectError bool - expectedOutput sdk.Dec + SwapFee sdk.Dec + ExitFee sdk.Dec + shouldErr bool }{ - { - name: "equal value", - baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 100), - quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 100), - expectError: false, - expectedOutput: sdk.MustNewDecFromStr("1"), - }, - { - name: "1:2 ratio", - baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 100), - quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 200), - expectError: false, - expectedOutput: sdk.MustNewDecFromStr("0.500000000000000000"), - }, - { - name: "2:1 ratio", - baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 200), - quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 100), - expectError: false, - expectedOutput: sdk.MustNewDecFromStr("2.000000000000000000"), - }, - { - name: "rounding after sigfig ratio", - baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 220), - quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 115), - expectError: false, - expectedOutput: sdk.MustNewDecFromStr("1.913043480000000000"), // ans is 1.913043478260869565, rounded is 1.91304348 - }, - { - name: "check number of sig figs", - baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 100), - quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 300), - expectError: false, - expectedOutput: sdk.MustNewDecFromStr("0.333333330000000000"), - }, - { - name: "check number of sig figs high sizes", - baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 343569192534), - quoteDenomPoolInput: sdk.NewCoin(quoteDenom, sdk.MustNewDecFromStr("186633424395479094888742").TruncateInt()), - expectError: false, - expectedOutput: sdk.MustNewDecFromStr("0.000000000001840877"), - }, - } - - for _, tc := range tests { - suite.SetupTest() - - poolId := suite.PrepareUni2PoolWithAssets( - tc.baseDenomPoolInput, - tc.quoteDenomPoolInput, - ) - - pool, err := suite.App.GAMMKeeper.GetPoolAndPoke(suite.Ctx, poolId) - suite.Require().NoError(err, "test: %s", tc.name) - balancerPool, isPool := pool.(*balancer.Pool) - suite.Require().True(isPool, "test: %s", tc.name) - - spotPrice, err := balancerPool.SpotPrice( - suite.Ctx, - tc.baseDenomPoolInput.Denom, - tc.quoteDenomPoolInput.Denom) - - if tc.expectError { - suite.Require().Error(err, "test: %s", tc.name) - } else { - suite.Require().NoError(err, "test: %s", tc.name) - suite.Require().True(spotPrice.Equal(tc.expectedOutput), - "test: %s\nSpot price wrong, got %s, expected %s\n", tc.name, - spotPrice, tc.expectedOutput) + // Should work + {defaultSwapFee, defaultExitFee, noErr}, + // Can't set the swap fee as negative + {sdk.NewDecWithPrec(-1, 2), defaultExitFee, wantErr}, + // Can't set the swap fee as 1 + {sdk.NewDec(1), defaultExitFee, wantErr}, + // Can't set the swap fee above 1 + {sdk.NewDecWithPrec(15, 1), defaultExitFee, wantErr}, + // Can't set the exit fee as negative + {defaultSwapFee, sdk.NewDecWithPrec(-1, 2), wantErr}, + // Can't set the exit fee as 1 + {defaultSwapFee, sdk.NewDec(1), wantErr}, + // Can't set the exit fee above 1 + {defaultSwapFee, sdk.NewDecWithPrec(15, 1), wantErr}, + } + + for i, params := range tests { + PoolParams := balancer.PoolParams{ + SwapFee: params.SwapFee, + ExitFee: params.ExitFee, } - } -} - -// TestCalculateAmountOutAndIn_InverseRelationship tests that the same amount of token is guaranteed upon -// sequential operation of CalcInAmtGivenOut and CalcOutAmtGivenIn. -func TestCalculateAmountOutAndIn_InverseRelationship(t *testing.T) { - type testcase struct { - denomOut string - initialPoolOut int64 - initialWeightOut int64 - initialCalcOut int64 - - denomIn string - initialPoolIn int64 - initialWeightIn int64 - } - - // For every test case in testcases, apply a swap fee in swapFeeCases. - testcases := []testcase{ - { - denomOut: "uosmo", - initialPoolOut: 1_000_000_000_000, - initialWeightOut: 100, - initialCalcOut: 100, - - denomIn: "ion", - initialPoolIn: 1_000_000_000_000, - initialWeightIn: 100, - }, - { - denomOut: "uosmo", - initialPoolOut: 1_000, - initialWeightOut: 100, - initialCalcOut: 100, - - denomIn: "ion", - initialPoolIn: 1_000_000, - initialWeightIn: 100, - }, - { - denomOut: "uosmo", - initialPoolOut: 1_000, - initialWeightOut: 100, - initialCalcOut: 100, - - denomIn: "ion", - initialPoolIn: 1_000_000, - initialWeightIn: 100, - }, - { - denomOut: "uosmo", - initialPoolOut: 1_000, - initialWeightOut: 200, - initialCalcOut: 100, - - denomIn: "ion", - initialPoolIn: 1_000_000, - initialWeightIn: 50, - }, - { - denomOut: "uosmo", - initialPoolOut: 1_000_000, - initialWeightOut: 200, - initialCalcOut: 100000, - - denomIn: "ion", - initialPoolIn: 1_000_000_000, - initialWeightIn: 50, - }, - } - - swapFeeCases := []string{"0", "0.001", "0.1", "0.5", "0.99"} - - getTestCaseName := func(tc testcase, swapFeeCase string) string { - return fmt.Sprintf("tokenOutInitial: %d, tokenInInitial: %d, initialOut: %d, swapFee: %s", - tc.initialPoolOut, - tc.initialPoolIn, - tc.initialCalcOut, - swapFeeCase, - ) - } - - for _, tc := range testcases { - for _, swapFee := range swapFeeCases { - t.Run(getTestCaseName(tc, swapFee), func(t *testing.T) { - ctx := createTestContext(t) - - poolAssetOut := balancer.PoolAsset{ - Token: sdk.NewInt64Coin(tc.denomOut, tc.initialPoolOut), - Weight: sdk.NewInt(tc.initialWeightOut), - } - - poolAssetIn := balancer.PoolAsset{ - Token: sdk.NewInt64Coin(tc.denomIn, tc.initialPoolIn), - Weight: sdk.NewInt(tc.initialWeightIn), - } - - swapFeeDec, err := sdk.NewDecFromStr(swapFee) - require.NoError(t, err) - - exitFeeDec, err := sdk.NewDecFromStr("0") - require.NoError(t, err) - - pool := createTestPool(t, swapFeeDec, exitFeeDec, poolAssetOut, poolAssetIn) - require.NotNil(t, pool) - - initialOut := sdk.NewInt64Coin(poolAssetOut.Token.Denom, tc.initialCalcOut) - initialOutCoins := sdk.NewCoins(initialOut) - - actualTokenIn, err := pool.CalcInAmtGivenOut(ctx, initialOutCoins, poolAssetIn.Token.Denom, swapFeeDec) - require.NoError(t, err) - - inverseTokenOut, err := pool.CalcOutAmtGivenIn(ctx, sdk.NewCoins(actualTokenIn), poolAssetOut.Token.Denom, swapFeeDec) - require.NoError(t, err) - - require.Equal(t, initialOut.Denom, inverseTokenOut.Denom) - - expected := initialOut.Amount.ToDec() - actual := inverseTokenOut.Amount.ToDec() - - // allow a rounding error of up to 1 for this relation - tol := sdk.NewDec(1) - require.True(osmoutils.DecApproxEq(t, expected, actual, tol)) - }) - } - } -} - -func TestCalcSingleAssetInAndOut_InverseRelationship(t *testing.T) { - type testcase struct { - initialPoolOut int64 - initialPoolIn int64 - initialWeightOut int64 - tokenOut int64 - initialWeightIn int64 - } - - // For every test case in testcases, apply a swap fee in swapFeeCases. - testcases := []testcase{ - { - initialPoolOut: 1_000_000_000_000, - tokenOut: 100, - initialWeightOut: 100, - initialWeightIn: 100, - }, - { - initialPoolOut: 1_000_000_000_000, - tokenOut: 100, - initialWeightOut: 50, - initialWeightIn: 100, - }, - { - initialPoolOut: 1_000_000_000_000, - tokenOut: 50, - initialWeightOut: 100, - initialWeightIn: 100, - }, - { - initialPoolOut: 1_000_000_000_000, - tokenOut: 100, - initialWeightOut: 100, - initialWeightIn: 50, - }, - { - initialPoolOut: 1_000_000, - tokenOut: 100, - initialWeightOut: 100, - initialWeightIn: 100, - }, - { - initialPoolOut: 2_351_333, - tokenOut: 7, - initialWeightOut: 148, - initialWeightIn: 57, - }, - { - initialPoolOut: 1_000, - tokenOut: 25, - initialWeightOut: 100, - initialWeightIn: 100, - }, - { - initialPoolOut: 1_000, - tokenOut: 26, - initialWeightOut: 100, - initialWeightIn: 100, - }, - } - - swapFeeCases := []string{"0", "0.001", "0.1", "0.5", "0.99"} - - getTestCaseName := func(tc testcase, swapFeeCase string) string { - return fmt.Sprintf("initialPoolOut: %d, initialCalcOut: %d, initialWeightOut: %d, initialWeightIn: %d, swapFee: %s", - tc.initialPoolOut, - tc.tokenOut, - tc.initialWeightOut, - tc.initialWeightIn, - swapFeeCase, - ) - } - - for _, tc := range testcases { - for _, swapFee := range swapFeeCases { - t.Run(getTestCaseName(tc, swapFee), func(t *testing.T) { - swapFeeDec, err := sdk.NewDecFromStr(swapFee) - require.NoError(t, err) - - initialPoolBalanceOut := sdk.NewInt(tc.initialPoolOut) - - initialWeightOut := sdk.NewInt(tc.initialWeightOut) - initialWeightIn := sdk.NewInt(tc.initialWeightIn) - - initialTotalShares := types.InitPoolSharesSupply.ToDec() - initialCalcTokenOut := sdk.NewInt(tc.tokenOut) - - actualSharesOut := balancer.CalcPoolSharesOutGivenSingleAssetIn( - initialPoolBalanceOut.ToDec(), - initialWeightOut.ToDec().Quo(initialWeightOut.Add(initialWeightIn).ToDec()), - initialTotalShares, - initialCalcTokenOut.ToDec(), - swapFeeDec, - ) - - inverseCalcTokenOut := balancer.CalcSingleAssetInGivenPoolSharesOut( - initialPoolBalanceOut.Add(initialCalcTokenOut).ToDec(), - initialWeightOut.ToDec().Quo(initialWeightOut.Add(initialWeightIn).ToDec()), - initialTotalShares.Add(actualSharesOut), - actualSharesOut, - swapFeeDec, - ) - - tol := sdk.NewDec(1) - require.True(osmoutils.DecApproxEq(t, initialCalcTokenOut.ToDec(), inverseCalcTokenOut, tol)) - }) + err := PoolParams.Validate(dummyPoolAssets) + if params.shouldErr { + require.Error(t, err, "unexpected lack of error, tc %v", i) + // Check that these are also caught if passed to the underlying pool creation func + _, err = balancer.NewBalancerPool(1, PoolParams, dummyPoolAssets, defaultFutureGovernor, defaultCurBlockTime) + require.Error(t, err) + } else { + require.NoError(t, err, "unexpected error, tc %v", i) } } } diff --git a/x/gamm/pool-models/balancer/balancer_pool.go b/x/gamm/pool-models/balancer/balancer_pool.go deleted file mode 100644 index b9e9b75d625..00000000000 --- a/x/gamm/pool-models/balancer/balancer_pool.go +++ /dev/null @@ -1,601 +0,0 @@ -package balancer - -import ( - "errors" - "fmt" - "sort" - "strings" - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - - "github.com/osmosis-labs/osmosis/v10/x/gamm/types" -) - -const ( - errMsgFormatNoPoolAssetFound = "can't find the PoolAsset (%s)" -) - -var ( - _ types.PoolI = &Pool{} - _ types.PoolAmountOutExtension = &Pool{} -) - -// NewPool returns a weighted CPMM pool with the provided parameters, and initial assets. -// Invariants that are assumed to be satisfied and not checked: -// (This is handled in ValidateBasic) -// * 2 <= len(assets) <= 8 -// * FutureGovernor is valid -// * poolID doesn't already exist -func NewBalancerPool(poolId uint64, balancerPoolParams PoolParams, assets []PoolAsset, futureGovernor string, blockTime time.Time) (Pool, error) { - poolAddr := types.NewPoolAddress(poolId) - - // pool thats created up to ensuring the assets and params are valid. - // We assume that FuturePoolGovernor is valid. - pool := &Pool{ - Address: poolAddr.String(), - Id: poolId, - PoolParams: PoolParams{}, - TotalWeight: sdk.ZeroInt(), - TotalShares: sdk.NewCoin(types.GetPoolShareDenom(poolId), types.InitPoolSharesSupply), - PoolAssets: nil, - FuturePoolGovernor: futureGovernor, - } - - err := pool.setInitialPoolAssets(assets) - if err != nil { - return Pool{}, err - } - - sortedPoolAssets := pool.GetAllPoolAssets() - err = balancerPoolParams.Validate(sortedPoolAssets) - if err != nil { - return Pool{}, err - } - - err = pool.setInitialPoolParams(balancerPoolParams, sortedPoolAssets, blockTime) - if err != nil { - return Pool{}, err - } - - return *pool, nil -} - -// GetAddress returns the address of a pool. -// If the pool address is not bech32 valid, it returns an empty address. -func (pa Pool) GetAddress() sdk.AccAddress { - addr, err := sdk.AccAddressFromBech32(pa.Address) - if err != nil { - panic(fmt.Sprintf("could not bech32 decode address of pool with id: %d", pa.GetId())) - } - return addr -} - -func (pa Pool) GetId() uint64 { - return pa.Id -} - -func (pa Pool) GetSwapFee(_ sdk.Context) sdk.Dec { - return pa.PoolParams.SwapFee -} - -func (pa Pool) GetTotalPoolLiquidity(_ sdk.Context) sdk.Coins { - return PoolAssetsCoins(pa.PoolAssets) -} - -func (pa Pool) GetExitFee(_ sdk.Context) sdk.Dec { - return pa.PoolParams.ExitFee -} - -func (pa Pool) GetPoolParams() PoolParams { - return pa.PoolParams -} - -func (pa Pool) GetTotalWeight() sdk.Int { - return pa.TotalWeight -} - -func (pa Pool) GetTotalShares() sdk.Int { - return pa.TotalShares.Amount -} - -func (pa *Pool) AddTotalShares(amt sdk.Int) { - pa.TotalShares.Amount = pa.TotalShares.Amount.Add(amt) -} - -func (pa *Pool) SubTotalShares(amt sdk.Int) { - pa.TotalShares.Amount = pa.TotalShares.Amount.Sub(amt) -} - -// setInitialPoolAssets sets the PoolAssets in the pool. -// It is only designed to be called at the pool's creation. -// If the same denom's PoolAsset exists, will return error. -// The list of PoolAssets must be sorted. This is done to enable fast searching for a PoolAsset by denomination. -// TODO: Unify story for validation of []PoolAsset, some is here, some is in CreatePool.ValidateBasic() -func (pa *Pool) setInitialPoolAssets(PoolAssets []PoolAsset) error { - exists := make(map[string]bool) - for _, asset := range pa.PoolAssets { - exists[asset.Token.Denom] = true - } - - newTotalWeight := pa.TotalWeight - scaledPoolAssets := make([]PoolAsset, 0, len(PoolAssets)) - - // TODO: Refactor this into PoolAsset.validate() - for _, asset := range PoolAssets { - if asset.Token.Amount.LTE(sdk.ZeroInt()) { - return fmt.Errorf("can't add the zero or negative balance of token") - } - - err := asset.ValidateWeight() - if err != nil { - return err - } - - if exists[asset.Token.Denom] { - return fmt.Errorf("same PoolAsset already exists") - } - exists[asset.Token.Denom] = true - - // Scale weight from the user provided weight to the correct internal weight - asset.Weight = asset.Weight.MulRaw(GuaranteedWeightPrecision) - scaledPoolAssets = append(scaledPoolAssets, asset) - newTotalWeight = newTotalWeight.Add(asset.Weight) - } - - // TODO: Change this to a more efficient sorted insert algorithm. - // Furthermore, consider changing the underlying data type to allow in-place modification if the - // number of PoolAssets is expected to be large. - pa.PoolAssets = append(pa.PoolAssets, scaledPoolAssets...) - SortPoolAssetsByDenom(pa.PoolAssets) - - pa.TotalWeight = newTotalWeight - - return nil -} - -// ValidateUserSpecifiedWeight ensures that a weight that is provided from user-input anywhere -// for creating a pool obeys the expected guarantees. -// Namely, that the weight is in the range [1, MaxUserSpecifiedWeight) -func ValidateUserSpecifiedWeight(weight sdk.Int) error { - if !weight.IsPositive() { - return sdkerrors.Wrap(types.ErrNotPositiveWeight, weight.String()) - } - - if weight.GTE(MaxUserSpecifiedWeight) { - return sdkerrors.Wrap(types.ErrWeightTooLarge, weight.String()) - } - return nil -} - -// setInitialPoolParams -func (pa *Pool) setInitialPoolParams(params PoolParams, sortedAssets []PoolAsset, curBlockTime time.Time) error { - pa.PoolParams = params - if params.SmoothWeightChangeParams != nil { - // set initial assets - initialWeights := make([]PoolAsset, len(sortedAssets)) - for i, v := range sortedAssets { - initialWeights[i] = PoolAsset{ - Weight: v.Weight, - Token: sdk.Coin{Denom: v.Token.Denom, Amount: sdk.ZeroInt()}, - } - } - params.SmoothWeightChangeParams.InitialPoolWeights = initialWeights - - // sort target weights by denom - targetPoolWeights := params.SmoothWeightChangeParams.TargetPoolWeights - SortPoolAssetsByDenom(targetPoolWeights) - - // scale target pool weights by GuaranteedWeightPrecision - for i, v := range targetPoolWeights { - err := ValidateUserSpecifiedWeight(v.Weight) - if err != nil { - return err - } - pa.PoolParams.SmoothWeightChangeParams.TargetPoolWeights[i] = PoolAsset{ - Weight: v.Weight.MulRaw(GuaranteedWeightPrecision), - Token: v.Token, - } - } - - // Set start time if not present. - if params.SmoothWeightChangeParams.StartTime.Unix() <= 0 { - // Per https://golang.org/pkg/time/#Time.Unix, should be timezone independent - params.SmoothWeightChangeParams.StartTime = time.Unix(curBlockTime.Unix(), 0) - } - } - - return nil -} - -// GetPoolAssets returns the denom's PoolAsset, If the PoolAsset doesn't exist, will return error. -// As above, it will search the denom's PoolAsset by using binary search. -// So, it is important to make sure that the PoolAssets are sorted. -func (pa Pool) GetPoolAsset(denom string) (PoolAsset, error) { - _, asset, err := pa.getPoolAssetAndIndex(denom) - return asset, err -} - -// Returns a pool asset, and its index. If err != nil, then the index will be valid. -func (pa Pool) getPoolAssetAndIndex(denom string) (int, PoolAsset, error) { - if denom == "" { - return -1, PoolAsset{}, fmt.Errorf("you tried to find the PoolAsset with empty denom") - } - - if len(pa.PoolAssets) == 0 { - return -1, PoolAsset{}, sdkerrors.Wrapf(types.ErrDenomNotFoundInPool, fmt.Sprintf(errMsgFormatNoPoolAssetFound, denom)) - } - - i := sort.Search(len(pa.PoolAssets), func(i int) bool { - PoolAssetA := pa.PoolAssets[i] - - compare := strings.Compare(PoolAssetA.Token.Denom, denom) - return compare >= 0 - }) - - if i < 0 || i >= len(pa.PoolAssets) { - return -1, PoolAsset{}, sdkerrors.Wrapf(types.ErrDenomNotFoundInPool, fmt.Sprintf(errMsgFormatNoPoolAssetFound, denom)) - } - - if pa.PoolAssets[i].Token.Denom != denom { - return -1, PoolAsset{}, sdkerrors.Wrapf(types.ErrDenomNotFoundInPool, fmt.Sprintf(errMsgFormatNoPoolAssetFound, denom)) - } - - return i, pa.PoolAssets[i], nil -} - -func (p Pool) parsePoolAssetsByDenoms(tokenADenom, tokenBDenom string) ( - Aasset PoolAsset, Basset PoolAsset, err error, -) { - Aasset, found1 := GetPoolAssetByDenom(p.PoolAssets, tokenADenom) - Basset, found2 := GetPoolAssetByDenom(p.PoolAssets, tokenBDenom) - if !(found1 && found2) { - return Aasset, Basset, errors.New("one of the provided pool denoms does not exist in pool") - } - return Aasset, Basset, nil -} - -func (p Pool) parsePoolAssets(tokensA sdk.Coins, tokenBDenom string) ( - tokenA sdk.Coin, Aasset PoolAsset, Basset PoolAsset, err error, -) { - if len(tokensA) != 1 { - return tokenA, Aasset, Basset, errors.New("expected tokensB to be of length one") - } - Aasset, Basset, err = p.parsePoolAssetsByDenoms(tokensA[0].Denom, tokenBDenom) - return tokensA[0], Aasset, Basset, err -} - -func (p Pool) parsePoolAssetsCoins(tokensA sdk.Coins, tokensB sdk.Coins) ( - Aasset PoolAsset, Basset PoolAsset, err error, -) { - if len(tokensB) != 1 { - return Aasset, Basset, errors.New("expected tokensA to be of length one") - } - _, Aasset, Basset, err = p.parsePoolAssets(tokensA, tokensB[0].Denom) - return Aasset, Basset, err -} - -func (p *Pool) IncreaseLiquidity(sharesOut sdk.Int, coinsIn sdk.Coins) { - err := p.addToPoolAssetBalances(coinsIn) - if err != nil { - panic(err) - } - p.AddTotalShares(sharesOut) -} - -func (pa *Pool) UpdatePoolAssetBalance(coin sdk.Coin) error { - // Check that PoolAsset exists. - assetIndex, existingAsset, err := pa.getPoolAssetAndIndex(coin.Denom) - if err != nil { - return err - } - - if coin.Amount.LTE(sdk.ZeroInt()) { - return fmt.Errorf("can't set the pool's balance of a token to be zero or negative") - } - - // Update the supply of the asset - existingAsset.Token = coin - pa.PoolAssets[assetIndex] = existingAsset - return nil -} - -func (pa *Pool) UpdatePoolAssetBalances(coins sdk.Coins) error { - // Ensures that there are no duplicate denoms, all denom's are valid, - // and amount is > 0 - err := coins.Validate() - if err != nil { - return fmt.Errorf("provided coins are invalid, %v", err) - } - - for _, coin := range coins { - // TODO: We may be able to make this log(|coins|) faster in how it - // looks up denom -> Coin by doing a multi-search, - // but as we don't anticipate |coins| to be large, we omit this. - err = pa.UpdatePoolAssetBalance(coin) - if err != nil { - return err - } - } - - return nil -} - -func (pa *Pool) addToPoolAssetBalances(coins sdk.Coins) error { - for _, coin := range coins { - i, poolAsset, err := pa.getPoolAssetAndIndex(coin.Denom) - if err != nil { - return err - } - poolAsset.Token.Amount = poolAsset.Token.Amount.Add(coin.Amount) - pa.PoolAssets[i] = poolAsset - } - return nil -} - -func (pa Pool) GetPoolAssets(denoms ...string) ([]PoolAsset, error) { - result := make([]PoolAsset, 0, len(denoms)) - - for _, denom := range denoms { - PoolAsset, err := pa.GetPoolAsset(denom) - if err != nil { - return nil, err - } - - result = append(result, PoolAsset) - } - - return result, nil -} - -func (pa Pool) GetAllPoolAssets() []PoolAsset { - copyslice := make([]PoolAsset, len(pa.PoolAssets)) - copy(copyslice, pa.PoolAssets) - return copyslice -} - -// updateAllWeights updates all of the pool's internal weights to be equal to -// the new weights. It assumes that `newWeights` are sorted by denomination, -// and only contain the same denominations as the pool already contains. -// This does not affect the asset balances. -// If any of the above are not satisfied, this will panic. -// (As all input to this should be generated from the state machine) -// TODO: (post-launch) If newWeights includes a new denomination, -// add the balance as well to the pool's internal measurements. -// TODO: (post-launch) If newWeights excludes an existing denomination, -// remove the weight from the pool, and figure out something to do -// with any remaining coin. -func (pa *Pool) updateAllWeights(newWeights []PoolAsset) { - if len(pa.PoolAssets) != len(newWeights) { - panic("updateAllWeights called with invalid input, len(newWeights) != len(existingWeights)") - } - totalWeight := sdk.ZeroInt() - for i, asset := range pa.PoolAssets { - if asset.Token.Denom != newWeights[i].Token.Denom { - panic(fmt.Sprintf("updateAllWeights called with invalid input, "+ - "expected new weights' %vth asset to be %v, got %v", - i, asset.Token.Denom, newWeights[i].Token.Denom)) - } - err := newWeights[i].ValidateWeight() - if err != nil { - panic("updateAllWeights: Tried to set an invalid weight") - } - pa.PoolAssets[i].Weight = newWeights[i].Weight - totalWeight = totalWeight.Add(pa.PoolAssets[i].Weight) - } - pa.TotalWeight = totalWeight -} - -// PokePool checks to see if the pool's token weights need to be updated, and -// if so, does so. -func (pa *Pool) PokePool(blockTime time.Time) { - // check if pool weights didn't change - poolWeightsChanging := pa.PoolParams.SmoothWeightChangeParams != nil - if !poolWeightsChanging { - return - } - - params := *pa.PoolParams.SmoothWeightChangeParams - - // The weights w(t) for the pool at time `t` is defined in one of three - // possible ways: - // - // 1. t <= start_time: w(t) = initial_pool_weights - // - // 2. start_time < t <= start_time + duration: - // w(t) = initial_pool_weights + (t - start_time) * - // (target_pool_weights - initial_pool_weights) / (duration) - // - // 3. t > start_time + duration: w(t) = target_pool_weights - switch { - case blockTime.Before(params.StartTime) || params.StartTime.Equal(blockTime): - // case 1: t <= start_time - return - - case blockTime.After(params.StartTime.Add(params.Duration)): - // case 2: start_time < t <= start_time + duration: - - // Update weights to be the target weights. - // - // TODO: When we add support for adding new assets via this method, ensure - // the new asset has some token sent with it. - pa.updateAllWeights(params.TargetPoolWeights) - - // we've finished updating the weights, so reset the following fields - pa.PoolParams.SmoothWeightChangeParams = nil - return - - default: - // case 3: t > start_time + duration: w(t) = target_pool_weights - - shiftedBlockTime := blockTime.Sub(params.StartTime).Milliseconds() - percentDurationElapsed := sdk.NewDec(shiftedBlockTime).QuoInt64(params.Duration.Milliseconds()) - - // If the duration elapsed is equal to the total time, or a rounding error - // makes it seem like it is, just set to target weight. - if percentDurationElapsed.GTE(sdk.OneDec()) { - pa.updateAllWeights(params.TargetPoolWeights) - return - } - - // below will be auto-truncated according to internal weight precision routine - totalWeightsDiff := subPoolAssetWeights(params.TargetPoolWeights, params.InitialPoolWeights) - scaledDiff := poolAssetsMulDec(totalWeightsDiff, percentDurationElapsed) - updatedWeights := addPoolAssetWeights(params.InitialPoolWeights, scaledDiff) - - pa.updateAllWeights(updatedWeights) - } -} - -func (pa Pool) GetTokenWeight(denom string) (sdk.Int, error) { - PoolAsset, err := pa.GetPoolAsset(denom) - if err != nil { - return sdk.Int{}, err - } - - return PoolAsset.Weight, nil -} - -func (pa Pool) GetTokenBalance(denom string) (sdk.Int, error) { - PoolAsset, err := pa.GetPoolAsset(denom) - if err != nil { - return sdk.Int{}, err - } - - return PoolAsset.Token.Amount, nil -} - -func (pa Pool) NumAssets() int { - return len(pa.PoolAssets) -} - -func (pa Pool) IsActive(ctx sdk.Context) bool { - return true -} - -func NewPoolParams(swapFee, exitFee sdk.Dec, params *SmoothWeightChangeParams) PoolParams { - return PoolParams{ - SwapFee: swapFee, - ExitFee: exitFee, - SmoothWeightChangeParams: params, - } -} - -func (params PoolParams) Validate(poolWeights []PoolAsset) error { - if params.ExitFee.IsNegative() { - return types.ErrNegativeExitFee - } - - if params.ExitFee.GTE(sdk.OneDec()) { - return types.ErrTooMuchExitFee - } - - if params.SwapFee.IsNegative() { - return types.ErrNegativeSwapFee - } - - if params.SwapFee.GTE(sdk.OneDec()) { - return types.ErrTooMuchSwapFee - } - - if params.SmoothWeightChangeParams != nil { - targetWeights := params.SmoothWeightChangeParams.TargetPoolWeights - // Ensure it has the right number of weights - if len(targetWeights) != len(poolWeights) { - return types.ErrPoolParamsInvalidNumDenoms - } - // Validate all user specified weights - for _, v := range targetWeights { - err := ValidateUserSpecifiedWeight(v.Weight) - if err != nil { - return err - } - } - // Ensure that all the target weight denoms are same as pool asset weights - sortedTargetPoolWeights := SortPoolAssetsOutOfPlaceByDenom(targetWeights) - sortedPoolWeights := SortPoolAssetsOutOfPlaceByDenom(poolWeights) - for i, v := range sortedPoolWeights { - if sortedTargetPoolWeights[i].Token.Denom != v.Token.Denom { - return types.ErrPoolParamsInvalidDenom - } - } - - // No start time validation needed - - // We do not need to validate InitialPoolWeights, as we set that ourselves - // in setInitialPoolParams - - // TODO: Is there anything else we can validate for duration? - if params.SmoothWeightChangeParams.Duration <= 0 { - return errors.New("params.SmoothWeightChangeParams must have a positive duration") - } - } - - return nil -} - -func (params PoolParams) GetPoolSwapFee() sdk.Dec { - return params.SwapFee -} - -func (params PoolParams) GetPoolExitFee() sdk.Dec { - return params.ExitFee -} - -// subPoolAssetWeights subtracts the weights of two different pool asset slices. -// It assumes that both pool assets have the same token denominations, -// with the denominations in the same order. -// Returned weights can (and probably will have some) be negative. -func subPoolAssetWeights(base []PoolAsset, other []PoolAsset) []PoolAsset { - weightDifference := make([]PoolAsset, len(base)) - // TODO: Consider deleting these panics for performance - if len(base) != len(other) { - panic("subPoolAssetWeights called with invalid input, len(base) != len(other)") - } - for i, asset := range base { - if asset.Token.Denom != other[i].Token.Denom { - panic(fmt.Sprintf("subPoolAssetWeights called with invalid input, "+ - "expected other's %vth asset to be %v, got %v", - i, asset.Token.Denom, other[i].Token.Denom)) - } - curWeightDiff := asset.Weight.Sub(other[i].Weight) - weightDifference[i] = PoolAsset{Token: asset.Token, Weight: curWeightDiff} - } - return weightDifference -} - -// addPoolAssetWeights adds the weights of two different pool asset slices. -// It assumes that both pool assets have the same token denominations, -// with the denominations in the same order. -// Returned weights can be negative. -func addPoolAssetWeights(base []PoolAsset, other []PoolAsset) []PoolAsset { - weightSum := make([]PoolAsset, len(base)) - // TODO: Consider deleting these panics for performance - if len(base) != len(other) { - panic("addPoolAssetWeights called with invalid input, len(base) != len(other)") - } - for i, asset := range base { - if asset.Token.Denom != other[i].Token.Denom { - panic(fmt.Sprintf("addPoolAssetWeights called with invalid input, "+ - "expected other's %vth asset to be %v, got %v", - i, asset.Token.Denom, other[i].Token.Denom)) - } - curWeightSum := asset.Weight.Add(other[i].Weight) - weightSum[i] = PoolAsset{Token: asset.Token, Weight: curWeightSum} - } - return weightSum -} - -// assumes 0 < d < 1 -func poolAssetsMulDec(base []PoolAsset, d sdk.Dec) []PoolAsset { - newWeights := make([]PoolAsset, len(base)) - for i, asset := range base { - // TODO: This can adversarially panic at the moment! (as can Pool.TotalWeight) - // Ensure this won't be able to panic in the future PR where we bound - // each assets weight, and add precision - newWeight := d.MulInt(asset.Weight).RoundInt() - newWeights[i] = PoolAsset{Token: asset.Token, Weight: newWeight} - } - return newWeights -} diff --git a/x/gamm/pool-models/balancer/balancer_pool_test.go b/x/gamm/pool-models/balancer/balancer_pool_test.go deleted file mode 100644 index 646b8d6f351..00000000000 --- a/x/gamm/pool-models/balancer/balancer_pool_test.go +++ /dev/null @@ -1,567 +0,0 @@ -package balancer - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -var ( - defaultSwapFee = sdk.MustNewDecFromStr("0.025") - defaultExitFee = sdk.MustNewDecFromStr("0.025") - defaultPoolId = uint64(10) - defaultBalancerPoolParams = PoolParams{ - SwapFee: defaultSwapFee, - ExitFee: defaultExitFee, - } - defaultFutureGovernor = "" - defaultCurBlockTime = time.Unix(1618700000, 0) - // - dummyPoolAssets = []PoolAsset{} - wantErr = true - noErr = false -) - -// Expected is un-scaled -func testTotalWeight(t *testing.T, expected sdk.Int, pool Pool) { - scaledExpected := expected.MulRaw(GuaranteedWeightPrecision) - require.Equal(t, - scaledExpected.String(), - pool.GetTotalWeight().String()) -} - -func TestBalancerPoolParams(t *testing.T) { - // Tests that creating a pool with the given pair of swapfee and exit fee - // errors or succeeds as intended. Furthermore, it checks that - // NewPool panics in the error case. - tests := []struct { - SwapFee sdk.Dec - ExitFee sdk.Dec - shouldErr bool - }{ - // Should work - {defaultSwapFee, defaultExitFee, noErr}, - // Can't set the swap fee as negative - {sdk.NewDecWithPrec(-1, 2), defaultExitFee, wantErr}, - // Can't set the swap fee as 1 - {sdk.NewDec(1), defaultExitFee, wantErr}, - // Can't set the swap fee above 1 - {sdk.NewDecWithPrec(15, 1), defaultExitFee, wantErr}, - // Can't set the exit fee as negative - {defaultSwapFee, sdk.NewDecWithPrec(-1, 2), wantErr}, - // Can't set the exit fee as 1 - {defaultSwapFee, sdk.NewDec(1), wantErr}, - // Can't set the exit fee above 1 - {defaultSwapFee, sdk.NewDecWithPrec(15, 1), wantErr}, - } - - for i, params := range tests { - PoolParams := PoolParams{ - SwapFee: params.SwapFee, - ExitFee: params.ExitFee, - } - err := PoolParams.Validate(dummyPoolAssets) - if params.shouldErr { - require.Error(t, err, "unexpected lack of error, tc %v", i) - // Check that these are also caught if passed to the underlying pool creation func - _, err = NewBalancerPool(1, PoolParams, dummyPoolAssets, defaultFutureGovernor, defaultCurBlockTime) - require.Error(t, err) - } else { - require.NoError(t, err, "unexpected error, tc %v", i) - } - } -} - -// TODO: Refactor this into multiple tests -func TestBalancerPoolUpdatePoolAssetBalance(t *testing.T) { - var poolId uint64 = 10 - - initialAssets := []PoolAsset{ - { - Weight: sdk.NewInt(100), - Token: sdk.NewCoin("test1", sdk.NewInt(50000)), - }, - { - Weight: sdk.NewInt(200), - Token: sdk.NewCoin("test2", sdk.NewInt(50000)), - }, - } - - pacc, err := NewBalancerPool(poolId, defaultBalancerPoolParams, initialAssets, defaultFutureGovernor, defaultCurBlockTime) - require.NoError(t, err) - - _, err = pacc.GetPoolAsset("unknown") - require.Error(t, err) - _, err = pacc.GetPoolAsset("") - require.Error(t, err) - - testTotalWeight(t, sdk.NewInt(300), pacc) - - // Break abstractions and start reasoning about the underlying internal representation's APIs. - // TODO: This test actually just needs to be refactored to not be doing this, and just - // create a different pool each time. - - err = pacc.setInitialPoolAssets([]PoolAsset{{ - Weight: sdk.NewInt(-1), - Token: sdk.NewCoin("negativeWeight", sdk.NewInt(50000)), - }}) - - require.Error(t, err) - - err = pacc.setInitialPoolAssets([]PoolAsset{{ - Weight: sdk.NewInt(0), - Token: sdk.NewCoin("zeroWeight", sdk.NewInt(50000)), - }}) - require.Error(t, err) - - err = pacc.UpdatePoolAssetBalance( - sdk.NewCoin("test1", sdk.NewInt(0))) - require.Error(t, err) - - err = pacc.UpdatePoolAssetBalance( - sdk.Coin{Denom: "test1", Amount: sdk.NewInt(-1)}, - ) - require.Error(t, err) - - err = pacc.UpdatePoolAssetBalance( - sdk.NewCoin("test1", sdk.NewInt(1))) - require.NoError(t, err) - - testTotalWeight(t, sdk.NewInt(300), pacc) - - PoolAsset, err := pacc.GetPoolAsset("test1") - require.NoError(t, err) - require.Equal(t, sdk.NewInt(1).String(), PoolAsset.Token.Amount.String()) -} - -func TestBalancerPoolAssetsWeightAndTokenBalance(t *testing.T) { - // TODO: Add more cases - // asset names should be i ascending order, starting from test1 - tests := []struct { - assets []PoolAsset - shouldErr bool - }{ - // weight 0 - { - []PoolAsset{ - { - Weight: sdk.NewInt(0), - Token: sdk.NewCoin("test1", sdk.NewInt(50000)), - }, - }, - wantErr, - }, - // negative weight - { - []PoolAsset{ - { - Weight: sdk.NewInt(-1), - Token: sdk.NewCoin("test1", sdk.NewInt(50000)), - }, - }, - wantErr, - }, - // 0 token amount - { - []PoolAsset{ - { - Weight: sdk.NewInt(100), - Token: sdk.NewCoin("test1", sdk.NewInt(0)), - }, - }, - wantErr, - }, - // negative token amount - { - []PoolAsset{ - { - Weight: sdk.NewInt(100), - Token: sdk.Coin{ - Denom: "test1", - Amount: sdk.NewInt(-1), - }, - }, - }, - wantErr, - }, - // total weight 300 - { - []PoolAsset{ - { - Weight: sdk.NewInt(200), - Token: sdk.NewCoin("test2", sdk.NewInt(50000)), - }, - { - Weight: sdk.NewInt(100), - Token: sdk.NewCoin("test1", sdk.NewInt(10000)), - }, - }, - noErr, - }, - // two of the same token - { - []PoolAsset{ - { - Weight: sdk.NewInt(200), - Token: sdk.NewCoin("test2", sdk.NewInt(50000)), - }, - { - Weight: sdk.NewInt(300), - Token: sdk.NewCoin("test1", sdk.NewInt(10000)), - }, - { - Weight: sdk.NewInt(100), - Token: sdk.NewCoin("test2", sdk.NewInt(10000)), - }, - }, - wantErr, - }, - // total weight 7300 - { - []PoolAsset{ - { - Weight: sdk.NewInt(200), - Token: sdk.NewCoin("test2", sdk.NewInt(50000)), - }, - { - Weight: sdk.NewInt(100), - Token: sdk.NewCoin("test1", sdk.NewInt(10000)), - }, - { - Weight: sdk.NewInt(7000), - Token: sdk.NewCoin("test3", sdk.NewInt(10000)), - }, - }, - noErr, - }, - } - - var poolId uint64 = 10 - - for i, tc := range tests { - pacc, err := NewBalancerPool(poolId, defaultBalancerPoolParams, tc.assets, defaultFutureGovernor, defaultCurBlockTime) - if tc.shouldErr { - require.Error(t, err, "unexpected lack of error, tc %v", i) - } else { - require.NoError(t, err, "unexpected error, tc %v", i) - expectedTotalWeight := sdk.ZeroInt() - for i, asset := range tc.assets { - expectedTotalWeight = expectedTotalWeight.Add(asset.Weight) - - // Ensure pool assets are sorted - require.Equal(t, "test"+fmt.Sprint(i+1), pacc.PoolAssets[i].Token.Denom) - } - testTotalWeight(t, expectedTotalWeight, pacc) - } - } -} - -// TODO: Figure out what parts of this test, if any, make sense. -func TestGetBalancerPoolAssets(t *testing.T) { - // Adds []PoolAssets, one after another - // if the addition doesn't error, adds the weight of the pool assets to a running total, - // and ensures the pool's total weight is equal to the expected. - // This also ensures that the pool assets remain sorted within the pool. - // Furthermore, it ensures that GetPoolAsset succeeds for everything in the pool, - // and fails for things not in it. - denomNotInPool := "xyzCoin" - - assets := []PoolAsset{ - { - Weight: sdk.NewInt(200), - Token: sdk.NewCoin("test2", sdk.NewInt(50000)), - }, - { - Weight: sdk.NewInt(100), - Token: sdk.NewCoin("test1", sdk.NewInt(10000)), - }, - { - Weight: sdk.NewInt(200), - Token: sdk.NewCoin("test3", sdk.NewInt(50000)), - }, - { - Weight: sdk.NewInt(100), - Token: sdk.NewCoin("test4", sdk.NewInt(10000)), - }, - } - - // TODO: We need way more robust test cases here, and should table drive these cases - pacc, err := NewBalancerPool(defaultPoolId, defaultBalancerPoolParams, assets, defaultFutureGovernor, defaultCurBlockTime) - require.NoError(t, err) - - // Hardcoded GetPoolAssets tests. - assets, err = pacc.GetPoolAssets("test1", "test2") - require.NoError(t, err) - require.Equal(t, 2, len(assets)) - - assets, err = pacc.GetPoolAssets("test1", "test2", "test3", "test4") - require.NoError(t, err) - require.Equal(t, 4, len(assets)) - - _, err = pacc.GetPoolAssets("test1", "test5") - require.Error(t, err) - _, err = pacc.GetPoolAssets(denomNotInPool) - require.Error(t, err) - - assets, err = pacc.GetPoolAssets() - require.NoError(t, err) - require.Equal(t, 0, len(assets)) -} - -func TestLBPParamsEmptyStartTime(t *testing.T) { - // Test that when the start time is empty, the pool - // sets its start time to be the first start time it is called on - defaultDuration := 100 * time.Second - - initialPoolAssets := []PoolAsset{ - { - Weight: sdk.NewInt(1), - Token: sdk.NewCoin("asset1", sdk.NewInt(1000)), - }, - { - Weight: sdk.NewInt(1), - Token: sdk.NewCoin("asset2", sdk.NewInt(1000)), - }, - } - - params := SmoothWeightChangeParams{ - Duration: defaultDuration, - TargetPoolWeights: []PoolAsset{ - { - Weight: sdk.NewInt(1), - Token: sdk.NewCoin("asset1", sdk.NewInt(0)), - }, - { - Weight: sdk.NewInt(2), - Token: sdk.NewCoin("asset2", sdk.NewInt(0)), - }, - }, - } - - pacc, err := NewBalancerPool(defaultPoolId, PoolParams{ - SmoothWeightChangeParams: ¶ms, - SwapFee: defaultSwapFee, - ExitFee: defaultExitFee, - }, initialPoolAssets, defaultFutureGovernor, defaultCurBlockTime) - require.NoError(t, err) - - // Consistency check that SmoothWeightChangeParams params are set - require.NotNil(t, pacc.PoolParams.SmoothWeightChangeParams) - // Ensure that the start time got set - require.Equal(t, pacc.PoolParams.SmoothWeightChangeParams.StartTime, defaultCurBlockTime) -} - -func TestBalancerPoolPokeTokenWeights(t *testing.T) { - // Set default date - defaultStartTime := time.Unix(1618703511, 0) - defaultStartTimeUnix := defaultStartTime.Unix() - defaultDuration := 100 * time.Second - floatGuaranteedPrecision := float64(GuaranteedWeightPrecision) - - // testCases don't need to be ordered by time. but the blockTime should be - // less than the end time of the SmoothWeightChange. Testing past the end time - // is already handled. - type testCase struct { - blockTime time.Time - expectedWeights []sdk.Int - } - - // Tests how the pool weights get updated via PokeTokenWeights at different block times. - // The framework underneath will automatically add tests for times before the start time, - // at the start time, at the end time, and after the end time. It is up to the test writer to - // test the behavior at times in-between. - tests := []struct { - // We take the initial weights from here - params SmoothWeightChangeParams - cases []testCase - }{ - { - // 1:1 pool, between asset1 and asset2 - // transitioning to a 1:2 pool - params: SmoothWeightChangeParams{ - StartTime: defaultStartTime, - Duration: defaultDuration, - InitialPoolWeights: []PoolAsset{ - { - Weight: sdk.NewInt(1), - Token: sdk.NewCoin("asset1", sdk.NewInt(0)), - }, - { - Weight: sdk.NewInt(1), - Token: sdk.NewCoin("asset2", sdk.NewInt(0)), - }, - }, - TargetPoolWeights: []PoolAsset{ - { - Weight: sdk.NewInt(1), - Token: sdk.NewCoin("asset1", sdk.NewInt(0)), - }, - { - Weight: sdk.NewInt(2), - Token: sdk.NewCoin("asset2", sdk.NewInt(0)), - }, - }, - }, - cases: []testCase{ - { - // Halfway through at 50 seconds elapsed - blockTime: time.Unix(defaultStartTimeUnix+50, 0), - expectedWeights: []sdk.Int{ - sdk.NewInt(1 * GuaranteedWeightPrecision), - // Halfway between 1 & 2 - sdk.NewInt(3 * GuaranteedWeightPrecision / 2), - }, - }, - { - // Quarter way through at 25 seconds elapsed - blockTime: time.Unix(defaultStartTimeUnix+25, 0), - expectedWeights: []sdk.Int{ - sdk.NewInt(1 * GuaranteedWeightPrecision), - // Quarter way between 1 & 2 = 1.25 - sdk.NewInt(int64(1.25 * floatGuaranteedPrecision)), - }, - }, - }, - }, - { - // 2:2 pool, between asset1 and asset2 - // transitioning to a 4:1 pool - params: SmoothWeightChangeParams{ - StartTime: defaultStartTime, - Duration: defaultDuration, - InitialPoolWeights: []PoolAsset{ - { - Weight: sdk.NewInt(2), - Token: sdk.NewCoin("asset1", sdk.NewInt(0)), - }, - { - Weight: sdk.NewInt(2), - Token: sdk.NewCoin("asset2", sdk.NewInt(0)), - }, - }, - TargetPoolWeights: []PoolAsset{ - { - Weight: sdk.NewInt(4), - Token: sdk.NewCoin("asset1", sdk.NewInt(0)), - }, - { - Weight: sdk.NewInt(1), - Token: sdk.NewCoin("asset2", sdk.NewInt(0)), - }, - }, - }, - cases: []testCase{ - { - // Halfway through at 50 seconds elapsed - blockTime: time.Unix(defaultStartTimeUnix+50, 0), - expectedWeights: []sdk.Int{ - // Halfway between 2 & 4 - sdk.NewInt(6 * GuaranteedWeightPrecision / 2), - // Halfway between 1 & 2 - sdk.NewInt(3 * GuaranteedWeightPrecision / 2), - }, - }, - { - // Quarter way through at 25 seconds elapsed - blockTime: time.Unix(defaultStartTimeUnix+25, 0), - expectedWeights: []sdk.Int{ - // Quarter way between 2 & 4 = 2.5 - sdk.NewInt(int64(2.5 * floatGuaranteedPrecision)), - // Quarter way between 2 & 1 = 1.75 - sdk.NewInt(int64(1.75 * floatGuaranteedPrecision)), - }, - }, - }, - }, - } - - // Add test cases at a time before the start, the start, the end, and a time after the end. - addDefaultCases := func(params SmoothWeightChangeParams, cases []testCase) []testCase { - // Set times one second before the start, and one second after the end - timeBeforeWeightChangeStart := time.Unix(params.StartTime.Unix()-1, 0) - timeAtWeightChangeEnd := params.StartTime.Add(params.Duration) - timeAfterWeightChangeEnd := time.Unix(timeAtWeightChangeEnd.Unix()+1, 0) - initialWeights := make([]sdk.Int, len(params.InitialPoolWeights)) - finalWeights := make([]sdk.Int, len(params.TargetPoolWeights)) - for i, v := range params.InitialPoolWeights { - initialWeights[i] = v.Weight.MulRaw(GuaranteedWeightPrecision) - } - for i, v := range params.TargetPoolWeights { - // Doesn't need to be scaled, due to this being done already in param initialization, - // and because params is only shallow copied - finalWeights[i] = v.Weight - } - // Set the test cases for times before the start, and the start - updatedCases := []testCase{ - { - blockTime: timeBeforeWeightChangeStart, - expectedWeights: initialWeights, - }, - { - blockTime: params.StartTime, - expectedWeights: initialWeights, - }, - } - // Append the provided cases - updatedCases = append(updatedCases, cases...) - finalCases := []testCase{ - { - blockTime: timeAtWeightChangeEnd, - expectedWeights: finalWeights, - }, - { - blockTime: timeAfterWeightChangeEnd, - expectedWeights: finalWeights, - }, - } - // Append the final cases - updatedCases = append(updatedCases, finalCases...) - return updatedCases - } - - for poolNum, tc := range tests { - paramsCopy := tc.params - // First we create the initial pool assets we will use - initialPoolAssets := make([]PoolAsset, len(paramsCopy.InitialPoolWeights)) - for i, asset := range paramsCopy.InitialPoolWeights { - assetCopy := PoolAsset{ - Weight: asset.Weight, - Token: sdk.NewInt64Coin(asset.Token.Denom, 10000), - } - initialPoolAssets[i] = assetCopy - } - // Initialize the pool - pacc, err := NewBalancerPool(uint64(poolNum), PoolParams{ - SwapFee: defaultSwapFee, - ExitFee: defaultExitFee, - SmoothWeightChangeParams: &tc.params, - }, initialPoolAssets, defaultFutureGovernor, defaultCurBlockTime) - require.NoError(t, err, "poolNumber %v", poolNum) - - // Consistency check that SmoothWeightChangeParams params are set - require.NotNil(t, pacc.PoolParams.SmoothWeightChangeParams) - - testCases := addDefaultCases(paramsCopy, tc.cases) - for caseNum, testCase := range testCases { - pacc.PokePool(testCase.blockTime) - - totalWeight := sdk.ZeroInt() - - for assetNum, asset := range pacc.GetAllPoolAssets() { - require.Equal(t, testCase.expectedWeights[assetNum], asset.Weight, - "Didn't get the expected weights, poolNumber %v, caseNumber %v, assetNumber %v", - poolNum, caseNum, assetNum) - - totalWeight = totalWeight.Add(asset.Weight) - } - - require.Equal(t, totalWeight, pacc.GetTotalWeight()) - } - // Should have been deleted by the last test case of after PokeTokenWeights pokes past end time. - require.Nil(t, pacc.PoolParams.SmoothWeightChangeParams) - } -} diff --git a/x/gamm/pool-models/balancer/marshal_test.go b/x/gamm/pool-models/balancer/marshal_test.go index 867886fde20..656c894ea88 100644 --- a/x/gamm/pool-models/balancer/marshal_test.go +++ b/x/gamm/pool-models/balancer/marshal_test.go @@ -1,4 +1,4 @@ -package balancer +package balancer_test import ( "encoding/hex" @@ -8,10 +8,12 @@ import ( "github.com/gogo/protobuf/proto" "github.com/stretchr/testify/require" + "github.com/osmosis-labs/osmosis/v10/x/gamm/pool-models/balancer" + sdk "github.com/cosmos/cosmos-sdk/types" ) -var ymlAssetTest = []PoolAsset{ +var ymlAssetTest = []balancer.PoolAsset{ { Weight: sdk.NewInt(200), Token: sdk.NewCoin("test2", sdk.NewInt(50000)), @@ -25,7 +27,7 @@ var ymlAssetTest = []PoolAsset{ func TestPoolJson(t *testing.T) { var poolId uint64 = 10 - jsonAssetTest := []PoolAsset{ + jsonAssetTest := []balancer.PoolAsset{ { Weight: sdk.NewInt(200), Token: sdk.NewCoin("test2", sdk.NewInt(50000)), @@ -35,7 +37,7 @@ func TestPoolJson(t *testing.T) { Token: sdk.NewCoin("test1", sdk.NewInt(10000)), }, } - pacc, err := NewBalancerPool(poolId, PoolParams{ + pacc, err := balancer.NewBalancerPool(poolId, balancer.PoolParams{ SwapFee: defaultSwapFee, ExitFee: defaultExitFee, }, jsonAssetTest, defaultFutureGovernor, defaultCurBlockTime) @@ -48,7 +50,7 @@ func TestPoolJson(t *testing.T) { require.NoError(t, err) require.Equal(t, string(bz1), string(bz)) - var a Pool + var a balancer.Pool require.NoError(t, json.Unmarshal(bz, &a)) require.Equal(t, pacc.String(), a.String()) } @@ -58,7 +60,7 @@ func TestPoolProtoMarshal(t *testing.T) { decodedByteArray, err := hex.DecodeString("0a3f6f736d6f316b727033387a7a63337a7a356173396e64716b79736b686b7a76367839653330636b63713567346c637375357770776371793073613364656132100a1a260a113235303030303030303030303030303030121132353030303030303030303030303030302a110a0c67616d6d2f706f6f6c2f3130120130321e0a0e0a05746573743112053130303030120c313037333734313832343030321e0a0e0a05746573743212053530303030120c3231343734383336343830303a0c333232313232353437323030") require.NoError(t, err) - pool2 := Pool{} + pool2 := balancer.Pool{} err = proto.Unmarshal(decodedByteArray, &pool2) require.NoError(t, err) @@ -67,7 +69,7 @@ func TestPoolProtoMarshal(t *testing.T) { require.Equal(t, pool2.PoolParams.ExitFee, defaultExitFee) require.Equal(t, pool2.FuturePoolGovernor, "") require.Equal(t, pool2.TotalShares, sdk.Coin{Denom: "gamm/pool/10", Amount: sdk.ZeroInt()}) - require.Equal(t, pool2.PoolAssets, []PoolAsset{ + require.Equal(t, pool2.PoolAssets, []balancer.PoolAsset{ { Token: sdk.Coin{ Denom: "test1", diff --git a/x/gamm/pool-models/balancer/pool.go b/x/gamm/pool-models/balancer/pool.go new file mode 100644 index 00000000000..18b248ce23c --- /dev/null +++ b/x/gamm/pool-models/balancer/pool.go @@ -0,0 +1,949 @@ +package balancer + +import ( + "errors" + "fmt" + "sort" + "strings" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/osmosis-labs/osmosis/v10/osmomath" + "github.com/osmosis-labs/osmosis/v10/x/gamm/pool-models/internal/cfmm_common" + "github.com/osmosis-labs/osmosis/v10/x/gamm/types" +) + +const ( + errMsgFormatSharesAmountNotPositive = "shares amount must be positive, was %d" + errMsgFormatTokenAmountNotPositive = "token amount must be positive, was %d" + errMsgFormatTokensLargerThanMax = "%d resulted tokens is larger than the max amount of %d" + errMsgFormatSharesLargerThanMax = "%d resulted shares is larger than the max amount of %d" + errMsgFormatFailedInterimLiquidityUpdate = "failed to update interim liquidity - pool asset %s does not exist" + errMsgFormatRepeatingPoolAssetsNotAllowed = "repeating pool assets not allowed, found %s" + errMsgFormatNoPoolAssetFound = "can't find the PoolAsset (%s)" + + v10Fork = 4713065 +) + +var ( + _ types.PoolI = &Pool{} + _ types.PoolAmountOutExtension = &Pool{} +) + +// NewPool returns a weighted CPMM pool with the provided parameters, and initial assets. +// Invariants that are assumed to be satisfied and not checked: +// (This is handled in ValidateBasic) +// * 2 <= len(assets) <= 8 +// * FutureGovernor is valid +// * poolID doesn't already exist +func NewBalancerPool(poolId uint64, balancerPoolParams PoolParams, assets []PoolAsset, futureGovernor string, blockTime time.Time) (Pool, error) { + poolAddr := types.NewPoolAddress(poolId) + + // pool thats created up to ensuring the assets and params are valid. + // We assume that FuturePoolGovernor is valid. + pool := &Pool{ + Address: poolAddr.String(), + Id: poolId, + PoolParams: PoolParams{}, + TotalWeight: sdk.ZeroInt(), + TotalShares: sdk.NewCoin(types.GetPoolShareDenom(poolId), types.InitPoolSharesSupply), + PoolAssets: nil, + FuturePoolGovernor: futureGovernor, + } + + err := pool.SetInitialPoolAssets(assets) + if err != nil { + return Pool{}, err + } + + sortedPoolAssets := pool.GetAllPoolAssets() + err = balancerPoolParams.Validate(sortedPoolAssets) + if err != nil { + return Pool{}, err + } + + err = pool.setInitialPoolParams(balancerPoolParams, sortedPoolAssets, blockTime) + if err != nil { + return Pool{}, err + } + + return *pool, nil +} + +// GetAddress returns the address of a pool. +// If the pool address is not bech32 valid, it returns an empty address. +func (pa Pool) GetAddress() sdk.AccAddress { + addr, err := sdk.AccAddressFromBech32(pa.Address) + if err != nil { + panic(fmt.Sprintf("could not bech32 decode address of pool with id: %d", pa.GetId())) + } + return addr +} + +func (pa Pool) GetId() uint64 { + return pa.Id +} + +func (pa Pool) GetSwapFee(_ sdk.Context) sdk.Dec { + return pa.PoolParams.SwapFee +} + +func (pa Pool) GetTotalPoolLiquidity(_ sdk.Context) sdk.Coins { + return PoolAssetsCoins(pa.PoolAssets) +} + +func (pa Pool) GetExitFee(_ sdk.Context) sdk.Dec { + return pa.PoolParams.ExitFee +} + +func (pa Pool) GetPoolParams() PoolParams { + return pa.PoolParams +} + +func (pa Pool) GetTotalWeight() sdk.Int { + return pa.TotalWeight +} + +func (pa Pool) GetTotalShares() sdk.Int { + return pa.TotalShares.Amount +} + +func (pa *Pool) AddTotalShares(amt sdk.Int) { + pa.TotalShares.Amount = pa.TotalShares.Amount.Add(amt) +} + +func (pa *Pool) SubTotalShares(amt sdk.Int) { + pa.TotalShares.Amount = pa.TotalShares.Amount.Sub(amt) +} + +// SetInitialPoolAssets sets the PoolAssets in the pool. It is only designed to +// be called at the pool's creation. If the same denom's PoolAsset exists, will +// return error. +// +// The list of PoolAssets must be sorted. This is done to enable fast searching +// for a PoolAsset by denomination. +// TODO: Unify story for validation of []PoolAsset, some is here, some is in +// CreatePool.ValidateBasic() +func (pa *Pool) SetInitialPoolAssets(PoolAssets []PoolAsset) error { + exists := make(map[string]bool) + for _, asset := range pa.PoolAssets { + exists[asset.Token.Denom] = true + } + + newTotalWeight := pa.TotalWeight + scaledPoolAssets := make([]PoolAsset, 0, len(PoolAssets)) + + // TODO: Refactor this into PoolAsset.validate() + for _, asset := range PoolAssets { + if asset.Token.Amount.LTE(sdk.ZeroInt()) { + return fmt.Errorf("can't add the zero or negative balance of token") + } + + err := asset.ValidateWeight() + if err != nil { + return err + } + + if exists[asset.Token.Denom] { + return fmt.Errorf("same PoolAsset already exists") + } + exists[asset.Token.Denom] = true + + // Scale weight from the user provided weight to the correct internal weight + asset.Weight = asset.Weight.MulRaw(GuaranteedWeightPrecision) + scaledPoolAssets = append(scaledPoolAssets, asset) + newTotalWeight = newTotalWeight.Add(asset.Weight) + } + + // TODO: Change this to a more efficient sorted insert algorithm. + // Furthermore, consider changing the underlying data type to allow in-place modification if the + // number of PoolAssets is expected to be large. + pa.PoolAssets = append(pa.PoolAssets, scaledPoolAssets...) + SortPoolAssetsByDenom(pa.PoolAssets) + + pa.TotalWeight = newTotalWeight + + return nil +} + +// setInitialPoolParams +func (pa *Pool) setInitialPoolParams(params PoolParams, sortedAssets []PoolAsset, curBlockTime time.Time) error { + pa.PoolParams = params + if params.SmoothWeightChangeParams != nil { + // set initial assets + initialWeights := make([]PoolAsset, len(sortedAssets)) + for i, v := range sortedAssets { + initialWeights[i] = PoolAsset{ + Weight: v.Weight, + Token: sdk.Coin{Denom: v.Token.Denom, Amount: sdk.ZeroInt()}, + } + } + params.SmoothWeightChangeParams.InitialPoolWeights = initialWeights + + // sort target weights by denom + targetPoolWeights := params.SmoothWeightChangeParams.TargetPoolWeights + SortPoolAssetsByDenom(targetPoolWeights) + + // scale target pool weights by GuaranteedWeightPrecision + for i, v := range targetPoolWeights { + err := ValidateUserSpecifiedWeight(v.Weight) + if err != nil { + return err + } + pa.PoolParams.SmoothWeightChangeParams.TargetPoolWeights[i] = PoolAsset{ + Weight: v.Weight.MulRaw(GuaranteedWeightPrecision), + Token: v.Token, + } + } + + // Set start time if not present. + if params.SmoothWeightChangeParams.StartTime.Unix() <= 0 { + // Per https://golang.org/pkg/time/#Time.Unix, should be timezone independent + params.SmoothWeightChangeParams.StartTime = time.Unix(curBlockTime.Unix(), 0) + } + } + + return nil +} + +// GetPoolAssets returns the denom's PoolAsset, If the PoolAsset doesn't exist, will return error. +// As above, it will search the denom's PoolAsset by using binary search. +// So, it is important to make sure that the PoolAssets are sorted. +func (pa Pool) GetPoolAsset(denom string) (PoolAsset, error) { + _, asset, err := pa.getPoolAssetAndIndex(denom) + return asset, err +} + +// Returns a pool asset, and its index. If err != nil, then the index will be valid. +func (pa Pool) getPoolAssetAndIndex(denom string) (int, PoolAsset, error) { + if denom == "" { + return -1, PoolAsset{}, fmt.Errorf("you tried to find the PoolAsset with empty denom") + } + + if len(pa.PoolAssets) == 0 { + return -1, PoolAsset{}, sdkerrors.Wrapf(types.ErrDenomNotFoundInPool, fmt.Sprintf(errMsgFormatNoPoolAssetFound, denom)) + } + + i := sort.Search(len(pa.PoolAssets), func(i int) bool { + PoolAssetA := pa.PoolAssets[i] + + compare := strings.Compare(PoolAssetA.Token.Denom, denom) + return compare >= 0 + }) + + if i < 0 || i >= len(pa.PoolAssets) { + return -1, PoolAsset{}, sdkerrors.Wrapf(types.ErrDenomNotFoundInPool, fmt.Sprintf(errMsgFormatNoPoolAssetFound, denom)) + } + + if pa.PoolAssets[i].Token.Denom != denom { + return -1, PoolAsset{}, sdkerrors.Wrapf(types.ErrDenomNotFoundInPool, fmt.Sprintf(errMsgFormatNoPoolAssetFound, denom)) + } + + return i, pa.PoolAssets[i], nil +} + +func (p Pool) parsePoolAssetsByDenoms(tokenADenom, tokenBDenom string) ( + Aasset PoolAsset, Basset PoolAsset, err error, +) { + Aasset, found1 := GetPoolAssetByDenom(p.PoolAssets, tokenADenom) + Basset, found2 := GetPoolAssetByDenom(p.PoolAssets, tokenBDenom) + if !(found1 && found2) { + return Aasset, Basset, errors.New("one of the provided pool denoms does not exist in pool") + } + return Aasset, Basset, nil +} + +func (p Pool) parsePoolAssets(tokensA sdk.Coins, tokenBDenom string) ( + tokenA sdk.Coin, Aasset PoolAsset, Basset PoolAsset, err error, +) { + if len(tokensA) != 1 { + return tokenA, Aasset, Basset, errors.New("expected tokensB to be of length one") + } + Aasset, Basset, err = p.parsePoolAssetsByDenoms(tokensA[0].Denom, tokenBDenom) + return tokensA[0], Aasset, Basset, err +} + +func (p Pool) parsePoolAssetsCoins(tokensA sdk.Coins, tokensB sdk.Coins) ( + Aasset PoolAsset, Basset PoolAsset, err error, +) { + if len(tokensB) != 1 { + return Aasset, Basset, errors.New("expected tokensA to be of length one") + } + _, Aasset, Basset, err = p.parsePoolAssets(tokensA, tokensB[0].Denom) + return Aasset, Basset, err +} + +func (p *Pool) IncreaseLiquidity(sharesOut sdk.Int, coinsIn sdk.Coins) { + err := p.addToPoolAssetBalances(coinsIn) + if err != nil { + panic(err) + } + p.AddTotalShares(sharesOut) +} + +func (pa *Pool) UpdatePoolAssetBalance(coin sdk.Coin) error { + // Check that PoolAsset exists. + assetIndex, existingAsset, err := pa.getPoolAssetAndIndex(coin.Denom) + if err != nil { + return err + } + + if coin.Amount.LTE(sdk.ZeroInt()) { + return fmt.Errorf("can't set the pool's balance of a token to be zero or negative") + } + + // Update the supply of the asset + existingAsset.Token = coin + pa.PoolAssets[assetIndex] = existingAsset + return nil +} + +func (pa *Pool) UpdatePoolAssetBalances(coins sdk.Coins) error { + // Ensures that there are no duplicate denoms, all denom's are valid, + // and amount is > 0 + err := coins.Validate() + if err != nil { + return fmt.Errorf("provided coins are invalid, %v", err) + } + + for _, coin := range coins { + // TODO: We may be able to make this log(|coins|) faster in how it + // looks up denom -> Coin by doing a multi-search, + // but as we don't anticipate |coins| to be large, we omit this. + err = pa.UpdatePoolAssetBalance(coin) + if err != nil { + return err + } + } + + return nil +} + +func (pa *Pool) addToPoolAssetBalances(coins sdk.Coins) error { + for _, coin := range coins { + i, poolAsset, err := pa.getPoolAssetAndIndex(coin.Denom) + if err != nil { + return err + } + poolAsset.Token.Amount = poolAsset.Token.Amount.Add(coin.Amount) + pa.PoolAssets[i] = poolAsset + } + return nil +} + +func (pa Pool) GetPoolAssets(denoms ...string) ([]PoolAsset, error) { + result := make([]PoolAsset, 0, len(denoms)) + + for _, denom := range denoms { + PoolAsset, err := pa.GetPoolAsset(denom) + if err != nil { + return nil, err + } + + result = append(result, PoolAsset) + } + + return result, nil +} + +func (pa Pool) GetAllPoolAssets() []PoolAsset { + copyslice := make([]PoolAsset, len(pa.PoolAssets)) + copy(copyslice, pa.PoolAssets) + return copyslice +} + +// updateAllWeights updates all of the pool's internal weights to be equal to +// the new weights. It assumes that `newWeights` are sorted by denomination, +// and only contain the same denominations as the pool already contains. +// This does not affect the asset balances. +// If any of the above are not satisfied, this will panic. +// (As all input to this should be generated from the state machine) +// TODO: (post-launch) If newWeights includes a new denomination, +// add the balance as well to the pool's internal measurements. +// TODO: (post-launch) If newWeights excludes an existing denomination, +// remove the weight from the pool, and figure out something to do +// with any remaining coin. +func (pa *Pool) updateAllWeights(newWeights []PoolAsset) { + if len(pa.PoolAssets) != len(newWeights) { + panic("updateAllWeights called with invalid input, len(newWeights) != len(existingWeights)") + } + totalWeight := sdk.ZeroInt() + for i, asset := range pa.PoolAssets { + if asset.Token.Denom != newWeights[i].Token.Denom { + panic(fmt.Sprintf("updateAllWeights called with invalid input, "+ + "expected new weights' %vth asset to be %v, got %v", + i, asset.Token.Denom, newWeights[i].Token.Denom)) + } + err := newWeights[i].ValidateWeight() + if err != nil { + panic("updateAllWeights: Tried to set an invalid weight") + } + pa.PoolAssets[i].Weight = newWeights[i].Weight + totalWeight = totalWeight.Add(pa.PoolAssets[i].Weight) + } + pa.TotalWeight = totalWeight +} + +// PokePool checks to see if the pool's token weights need to be updated, and +// if so, does so. Currently doesn't do anything outside out LBPs. +func (pa *Pool) PokePool(blockTime time.Time) { + // check if pool weights didn't change + poolWeightsChanging := pa.PoolParams.SmoothWeightChangeParams != nil + if !poolWeightsChanging { + return + } + + params := *pa.PoolParams.SmoothWeightChangeParams + + // The weights w(t) for the pool at time `t` is defined in one of three + // possible ways: + // + // 1. t <= start_time: w(t) = initial_pool_weights + // + // 2. start_time < t <= start_time + duration: + // w(t) = initial_pool_weights + (t - start_time) * + // (target_pool_weights - initial_pool_weights) / (duration) + // + // 3. t > start_time + duration: w(t) = target_pool_weights + switch { + case blockTime.Before(params.StartTime) || params.StartTime.Equal(blockTime): + // case 1: t <= start_time + return + + case blockTime.After(params.StartTime.Add(params.Duration)): + // case 2: start_time < t <= start_time + duration: + + // Update weights to be the target weights. + // + // TODO: When we add support for adding new assets via this method, ensure + // the new asset has some token sent with it. + pa.updateAllWeights(params.TargetPoolWeights) + + // we've finished updating the weights, so reset the following fields + pa.PoolParams.SmoothWeightChangeParams = nil + return + + default: + // case 3: t > start_time + duration: w(t) = target_pool_weights + + shiftedBlockTime := blockTime.Sub(params.StartTime).Milliseconds() + percentDurationElapsed := sdk.NewDec(shiftedBlockTime).QuoInt64(params.Duration.Milliseconds()) + + // If the duration elapsed is equal to the total time, or a rounding error + // makes it seem like it is, just set to target weight. + if percentDurationElapsed.GTE(sdk.OneDec()) { + pa.updateAllWeights(params.TargetPoolWeights) + return + } + + // below will be auto-truncated according to internal weight precision routine + totalWeightsDiff := subPoolAssetWeights(params.TargetPoolWeights, params.InitialPoolWeights) + scaledDiff := poolAssetsMulDec(totalWeightsDiff, percentDurationElapsed) + updatedWeights := addPoolAssetWeights(params.InitialPoolWeights, scaledDiff) + + pa.updateAllWeights(updatedWeights) + } +} + +func (pa Pool) GetTokenWeight(denom string) (sdk.Int, error) { + PoolAsset, err := pa.GetPoolAsset(denom) + if err != nil { + return sdk.Int{}, err + } + + return PoolAsset.Weight, nil +} + +func (pa Pool) GetTokenBalance(denom string) (sdk.Int, error) { + PoolAsset, err := pa.GetPoolAsset(denom) + if err != nil { + return sdk.Int{}, err + } + + return PoolAsset.Token.Amount, nil +} + +func (pa Pool) NumAssets() int { + return len(pa.PoolAssets) +} + +func (pa Pool) IsActive(ctx sdk.Context) bool { + return true +} + +// CalcOutAmtGivenIn calculates tokens to be swapped out given the provided +// amount and fee deducted, using solveConstantFunctionInvariant. +func (p Pool) CalcOutAmtGivenIn( + ctx sdk.Context, + tokensIn sdk.Coins, + tokenOutDenom string, + swapFee sdk.Dec, +) (sdk.Coin, error) { + tokenIn, poolAssetIn, poolAssetOut, err := p.parsePoolAssets(tokensIn, tokenOutDenom) + if err != nil { + return sdk.Coin{}, err + } + + tokenAmountInAfterFee := tokenIn.Amount.ToDec().Mul(sdk.OneDec().Sub(swapFee)) + poolTokenInBalance := poolAssetIn.Token.Amount.ToDec() + poolPostSwapInBalance := poolTokenInBalance.Add(tokenAmountInAfterFee) + + // deduct swapfee on the tokensIn + // delta balanceOut is positive(tokens inside the pool decreases) + tokenAmountOut := solveConstantFunctionInvariant( + poolTokenInBalance, + poolPostSwapInBalance, + poolAssetIn.Weight.ToDec(), + poolAssetOut.Token.Amount.ToDec(), + poolAssetOut.Weight.ToDec(), + ) + + // We ignore the decimal component, as we round down the token amount out. + tokenAmountOutInt := tokenAmountOut.TruncateInt() + if !tokenAmountOutInt.IsPositive() { + return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") + } + + return sdk.NewCoin(tokenOutDenom, tokenAmountOutInt), nil +} + +// SwapOutAmtGivenIn is a mutative method for CalcOutAmtGivenIn, which includes the actual swap. +func (p *Pool) SwapOutAmtGivenIn( + ctx sdk.Context, + tokensIn sdk.Coins, + tokenOutDenom string, + swapFee sdk.Dec, +) ( + tokenOut sdk.Coin, err error, +) { + tokenOutCoin, err := p.CalcOutAmtGivenIn(ctx, tokensIn, tokenOutDenom, swapFee) + if err != nil { + return sdk.Coin{}, err + } + + err = p.applySwap(ctx, tokensIn, sdk.Coins{tokenOutCoin}) + if err != nil { + return sdk.Coin{}, err + } + return tokenOutCoin, nil +} + +// CalcInAmtGivenOut calculates token to be provided, fee added, +// given the swapped out amount, using solveConstantFunctionInvariant. +func (p Pool) CalcInAmtGivenOut( + ctx sdk.Context, tokensOut sdk.Coins, tokenInDenom string, swapFee sdk.Dec) ( + tokenIn sdk.Coin, err error, +) { + tokenOut, poolAssetOut, poolAssetIn, err := p.parsePoolAssets(tokensOut, tokenInDenom) + if err != nil { + return sdk.Coin{}, err + } + + // delta balanceOut is positive(tokens inside the pool decreases) + poolTokenOutBalance := poolAssetOut.Token.Amount.ToDec() + poolPostSwapOutBalance := poolTokenOutBalance.Sub(tokenOut.Amount.ToDec()) + // (x_0)(y_0) = (x_0 + in)(y_0 - out) + tokenAmountIn := solveConstantFunctionInvariant( + poolTokenOutBalance, poolPostSwapOutBalance, poolAssetOut.Weight.ToDec(), + poolAssetIn.Token.Amount.ToDec(), poolAssetIn.Weight.ToDec()).Neg() + + // We deduct a swap fee on the input asset. The swap happens by following the invariant curve on the input * (1 - swap fee) + // and then the swap fee is added to the pool. + // Thus in order to give X amount out, we solve the invariant for the invariant input. However invariant input = (1 - swapfee) * trade input. + // Therefore we divide by (1 - swapfee) here + tokenAmountInBeforeFee := tokenAmountIn.Quo(sdk.OneDec().Sub(swapFee)) + + // We round up tokenInAmt, as this is whats charged for the swap, for the precise amount out. + // Otherwise, the pool would under-charge by this rounding error. + tokenInAmt := tokenAmountInBeforeFee.Ceil().TruncateInt() + + if !tokenInAmt.IsPositive() { + return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") + } + return sdk.NewCoin(tokenInDenom, tokenInAmt), nil +} + +// SwapInAmtGivenOut is a mutative method for CalcOutAmtGivenIn, which includes the actual swap. +func (p *Pool) SwapInAmtGivenOut( + ctx sdk.Context, tokensOut sdk.Coins, tokenInDenom string, swapFee sdk.Dec) ( + tokenIn sdk.Coin, err error, +) { + tokenInCoin, err := p.CalcInAmtGivenOut(ctx, tokensOut, tokenInDenom, swapFee) + if err != nil { + return sdk.Coin{}, err + } + + err = p.applySwap(ctx, sdk.Coins{tokenInCoin}, tokensOut) + if err != nil { + return sdk.Coin{}, err + } + return tokenInCoin, nil +} + +// ApplySwap. +func (p *Pool) applySwap(ctx sdk.Context, tokensIn sdk.Coins, tokensOut sdk.Coins) error { + // Also ensures that len(tokensIn) = 1 = len(tokensOut) + inPoolAsset, outPoolAsset, err := p.parsePoolAssetsCoins(tokensIn, tokensOut) + if err != nil { + return err + } + inPoolAsset.Token.Amount = inPoolAsset.Token.Amount.Add(tokensIn[0].Amount) + outPoolAsset.Token.Amount = outPoolAsset.Token.Amount.Sub(tokensOut[0].Amount) + + return p.UpdatePoolAssetBalances(sdk.NewCoins( + inPoolAsset.Token, + outPoolAsset.Token, + )) +} + +// SpotPrice returns the spot price of the pool +// This is the weight-adjusted balance of the tokens in the pool. +// In order reduce the propagated effect of incorrect trailing digits, +// we take the ratio of weights and divide this by ratio of supplies +// this is equivalent to spot_price = (Base_supply / Weight_base) / (Quote_supply / Weight_quote) +// but cancels out the common term in weight. +// +// panics if pool is misconfigured and has any weight as 0. +func (p Pool) SpotPrice(ctx sdk.Context, baseAsset, quoteAsset string) (sdk.Dec, error) { + quote, base, err := p.parsePoolAssetsByDenoms(quoteAsset, baseAsset) + if err != nil { + return sdk.Dec{}, err + } + if base.Weight.IsZero() || quote.Weight.IsZero() { + return sdk.Dec{}, errors.New("pool is misconfigured, got 0 weight") + } + + // spot_price = (Base_supply / Weight_base) / (Quote_supply / Weight_quote) + // spot_price = (weight_quote / weight_base) * (base_supply / quote_supply) + invWeightRatio := quote.Weight.ToDec().Quo(base.Weight.ToDec()) + supplyRatio := base.Token.Amount.ToDec().Quo(quote.Token.Amount.ToDec()) + fullRatio := supplyRatio.Mul(invWeightRatio) + // we want to round this to `SigFigs` of precision + ratio := osmomath.SigFigRound(fullRatio, types.SigFigs) + return ratio, nil +} + +// calcPoolOutGivenSingleIn - balance pAo. +func (p *Pool) calcSingleAssetJoin(tokenIn sdk.Coin, swapFee sdk.Dec, tokenInPoolAsset PoolAsset, totalShares sdk.Int) (numShares sdk.Int, err error) { + _, err = p.GetPoolAsset(tokenIn.Denom) + if err != nil { + return sdk.ZeroInt(), err + } + + totalWeight := p.GetTotalWeight() + if totalWeight.IsZero() { + return sdk.ZeroInt(), errors.New("pool misconfigured, total weight = 0") + } + normalizedWeight := tokenInPoolAsset.Weight.ToDec().Quo(totalWeight.ToDec()) + return calcPoolSharesOutGivenSingleAssetIn( + tokenInPoolAsset.Token.Amount.ToDec(), + normalizedWeight, + totalShares.ToDec(), + tokenIn.Amount.ToDec(), + swapFee, + ).TruncateInt(), nil +} + +// JoinPool calculates the number of shares needed given tokensIn with swapFee applied. +// It updates the liquidity if the pool is joined successfully. If not, returns error. +// and updates pool accordingly. +func (p *Pool) JoinPool(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, err error) { + numShares, newLiquidity, err := p.CalcJoinPoolShares(ctx, tokensIn, swapFee) + if err != nil { + return sdk.Int{}, err + } + + // update pool with the calculated share and liquidity needed to join pool + p.IncreaseLiquidity(numShares, newLiquidity) + return numShares, nil +} + +func (p *Pool) calcJoinPoolSharesBroken(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, newLiquidity sdk.Coins, err error) { + poolAssets := p.GetAllPoolAssets() + poolAssetsByDenom := make(map[string]PoolAsset) + for _, poolAsset := range poolAssets { + poolAssetsByDenom[poolAsset.Token.Denom] = poolAsset + } + + totalShares := p.GetTotalShares() + + if tokensIn.Len() == 1 { + numShares, err = p.calcSingleAssetJoin(tokensIn[0], swapFee, poolAssetsByDenom[tokensIn[0].Denom], totalShares) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + + newLiquidity = tokensIn + + return numShares, newLiquidity, nil + } else if tokensIn.Len() != p.NumAssets() { + return sdk.ZeroInt(), sdk.NewCoins(), errors.New("balancer pool only supports LP'ing with one asset or all assets in pool") + } + + // Add all exact coins we can (no swap). ctx arg doesn't matter for Balancer. + numShares, remCoins, err := cfmm_common.MaximalExactRatioJoinBroken(p, sdk.Context{}, tokensIn) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + + // update liquidity for accurate calcSingleAssetJoin calculation + newLiquidity = tokensIn.Sub(remCoins) + for _, coin := range newLiquidity { + poolAsset := poolAssetsByDenom[coin.Denom] + poolAsset.Token.Amount = poolAssetsByDenom[coin.Denom].Token.Amount.Add(coin.Amount) + poolAssetsByDenom[coin.Denom] = poolAsset + } + + totalShares = totalShares.Add(numShares) + + // If there are coins that couldn't be perfectly joined, do single asset joins + // for each of them. + if !remCoins.Empty() { + for _, coin := range remCoins { + newShares, err := p.calcSingleAssetJoin(coin, swapFee, poolAssetsByDenom[coin.Denom], totalShares) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + + newLiquidity = newLiquidity.Add(coin) + numShares = numShares.Add(newShares) + } + } + + return numShares, newLiquidity, nil +} + +// CalcJoinPoolShares calculates the number of shares created to join pool with the provided amount of `tokenIn`. +// The input tokens must either be: +// - a single token +// - contain exactly the same tokens as the pool contains +// +// It returns the number of shares created, the amount of coins actually joined into the pool +// (in case of not being able to fully join), or an error. +func (p *Pool) CalcJoinPoolShares(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, tokensJoined sdk.Coins, err error) { + if ctx.BlockHeight() < v10Fork { + return p.calcJoinPoolSharesBroken(ctx, tokensIn, swapFee) + } + // 1) Get pool current liquidity + and token weights + // 2) If single token provided, do single asset join and exit. + // 3) If multi-asset join, first do as much of a join as we can with no swaps. + // 4) Update pool shares / liquidity / remaining tokens to join accordingly + // 5) For every remaining token to LP, do a single asset join, and update pool shares / liquidity. + // + // Note that all single asset joins do incur swap fee. + // + // Since CalcJoinPoolShares is non-mutative, the steps for updating pool shares / liquidity are + // more complex / don't just alter the state. + // We should simplify this logic further in the future, using balancer multi-join equations. + + // 1) get all 'pool assets' (aka current pool liquidity + balancer weight) + poolAssetsByDenom, err := getPoolAssetsByDenom(p.GetAllPoolAssets()) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + + totalShares := p.GetTotalShares() + if tokensIn.Len() == 1 { + // 2) Single token provided, so do single asset join and exit. + numShares, err = p.calcSingleAssetJoin(tokensIn[0], swapFee, poolAssetsByDenom[tokensIn[0].Denom], totalShares) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + // we join all the tokens. + tokensJoined = tokensIn + return numShares, tokensJoined, nil + } else if tokensIn.Len() != p.NumAssets() { + return sdk.ZeroInt(), sdk.NewCoins(), errors.New("balancer pool only supports LP'ing with one asset or all assets in pool") + } + + // 3) JoinPoolNoSwap with as many tokens as we can. (What is in 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. + // if remaining coins is empty, logic is done (we joined all tokensIn) + numShares, remainingTokensIn, err := cfmm_common.MaximalExactRatioJoin(p, sdk.Context{}, tokensIn) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + if remainingTokensIn.Empty() { + tokensJoined = tokensIn + return numShares, tokensJoined, nil + } + + // 4) Still more coins to join, so we update the effective pool state here to account for + // join that just happened. + // * We add the joined coins to our "current pool liquidity" object (poolAssetsByDenom) + // * We increment a variable for our "newTotalShares" to add in the shares that've been added. + tokensJoined = tokensIn.Sub(remainingTokensIn) + if err := updateIntermediaryPoolAssetsLiquidity(tokensJoined, poolAssetsByDenom); err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + newTotalShares := totalShares.Add(numShares) + + // 5) Now single asset join each remaining coin. + newNumSharesFromRemaining, newLiquidityFromRemaining, err := p.calcJoinSingleAssetTokensIn(remainingTokensIn, newTotalShares, poolAssetsByDenom, swapFee) + if err != nil { + return sdk.ZeroInt(), sdk.NewCoins(), err + } + // update total amount LP'd variable, and total new LP shares variable, run safety check, and return + numShares = numShares.Add(newNumSharesFromRemaining) + tokensJoined = tokensJoined.Add(newLiquidityFromRemaining...) + + 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 +} + +// calcJoinSingleAssetTokensIn attempts to calculate single +// asset join for all tokensIn given totalShares in pool, +// poolAssetsByDenom and swapFee. totalShares is the number +// of shares in pool before beginnning to join any of the tokensIn. +// +// Returns totalNewShares and totalNewLiquidity from joining all tokensIn +// by mimicking individually single asset joining each. +// or error if fails to calculate join for any of the tokensIn. +func (p *Pool) calcJoinSingleAssetTokensIn(tokensIn sdk.Coins, totalShares sdk.Int, poolAssetsByDenom map[string]PoolAsset, swapFee sdk.Dec) (sdk.Int, sdk.Coins, error) { + totalNewShares := sdk.ZeroInt() + totalNewLiquidity := sdk.NewCoins() + for _, coin := range tokensIn { + newShares, err := p.calcSingleAssetJoin(coin, swapFee, poolAssetsByDenom[coin.Denom], totalShares.Add(totalNewShares)) + if err != nil { + return sdk.ZeroInt(), sdk.Coins{}, err + } + + totalNewLiquidity = totalNewLiquidity.Add(coin) + totalNewShares = totalNewShares.Add(newShares) + } + return totalNewShares, totalNewLiquidity, nil +} + +func (p *Pool) ExitPool(ctx sdk.Context, exitingShares sdk.Int, exitFee sdk.Dec) (exitingCoins sdk.Coins, err error) { + exitingCoins, err = p.CalcExitPoolShares(ctx, exitingShares, exitFee) + if err != nil { + return sdk.Coins{}, err + } + + if err := p.exitPool(ctx, exitingCoins, exitingShares); err != nil { + return sdk.Coins{}, err + } + + return exitingCoins, nil +} + +// exitPool exits the pool given exitingCoins and exitingShares. +// updates the pool's liquidity and totalShares. +func (p *Pool) exitPool(ctx sdk.Context, exitingCoins sdk.Coins, exitingShares sdk.Int) error { + balances := p.GetTotalPoolLiquidity(ctx).Sub(exitingCoins) + if err := p.UpdatePoolAssetBalances(balances); err != nil { + return err + } + + totalShares := p.GetTotalShares() + p.TotalShares = sdk.NewCoin(p.TotalShares.Denom, totalShares.Sub(exitingShares)) + + return nil +} + +func (p *Pool) CalcExitPoolShares(ctx sdk.Context, exitingShares sdk.Int, exitFee sdk.Dec) (exitedCoins sdk.Coins, err error) { + return cfmm_common.CalcExitPool(ctx, p, exitingShares, exitFee) +} + +func (p *Pool) CalcTokenInShareAmountOut( + ctx sdk.Context, + tokenInDenom string, + shareOutAmount sdk.Int, + swapFee sdk.Dec, +) (tokenInAmount sdk.Int, err error) { + _, poolAssetIn, err := p.getPoolAssetAndIndex(tokenInDenom) + if err != nil { + return sdk.Int{}, err + } + + normalizedWeight := poolAssetIn.Weight.ToDec().Quo(p.GetTotalWeight().ToDec()) + + // We round up tokenInAmount, as this is whats charged for the swap, for the precise amount out. + // Otherwise, the pool would under-charge by this rounding error. + tokenInAmount = calcSingleAssetInGivenPoolSharesOut( + poolAssetIn.Token.Amount.ToDec(), + normalizedWeight, + p.GetTotalShares().ToDec(), + shareOutAmount.ToDec(), + swapFee, + ).Ceil().TruncateInt() + + if !tokenInAmount.IsPositive() { + return sdk.Int{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, errMsgFormatTokenAmountNotPositive, tokenInAmount.Int64()) + } + + return tokenInAmount, nil +} + +func (p *Pool) JoinPoolTokenInMaxShareAmountOut( + ctx sdk.Context, + tokenInDenom string, + shareOutAmount sdk.Int, +) (tokenInAmount sdk.Int, err error) { + _, poolAssetIn, err := p.getPoolAssetAndIndex(tokenInDenom) + if err != nil { + return sdk.Int{}, err + } + + normalizedWeight := poolAssetIn.Weight.ToDec().Quo(p.GetTotalWeight().ToDec()) + + tokenInAmount = calcSingleAssetInGivenPoolSharesOut( + poolAssetIn.Token.Amount.ToDec(), + normalizedWeight, + p.GetTotalShares().ToDec(), + shareOutAmount.ToDec(), + p.GetSwapFee(ctx), + ).TruncateInt() + + if !tokenInAmount.IsPositive() { + return sdk.Int{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, errMsgFormatTokenAmountNotPositive, tokenInAmount.Int64()) + } + + poolAssetIn.Token.Amount = poolAssetIn.Token.Amount.Add(tokenInAmount) + err = p.UpdatePoolAssetBalance(poolAssetIn.Token) + if err != nil { + return sdk.Int{}, err + } + + return tokenInAmount, nil +} + +func (p *Pool) ExitSwapExactAmountOut( + ctx sdk.Context, + tokenOut sdk.Coin, + shareInMaxAmount sdk.Int, +) (shareInAmount sdk.Int, err error) { + _, poolAssetOut, err := p.getPoolAssetAndIndex(tokenOut.Denom) + if err != nil { + return sdk.Int{}, err + } + + sharesIn := calcPoolSharesInGivenSingleAssetOut( + poolAssetOut.Token.Amount.ToDec(), + poolAssetOut.Weight.ToDec().Quo(p.TotalWeight.ToDec()), + p.GetTotalShares().ToDec(), + tokenOut.Amount.ToDec(), + p.GetSwapFee(ctx), + p.GetExitFee(ctx), + ).TruncateInt() + + if !sharesIn.IsPositive() { + return sdk.Int{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, errMsgFormatSharesAmountNotPositive, sharesIn.Int64()) + } + + if sharesIn.GT(shareInMaxAmount) { + return sdk.Int{}, sdkerrors.Wrapf(types.ErrLimitMaxAmount, errMsgFormatSharesLargerThanMax, sharesIn.Int64(), shareInMaxAmount.Uint64()) + } + + if err := p.exitPool(ctx, sdk.NewCoins(tokenOut), sharesIn); err != nil { + return sdk.Int{}, err + } + + return sharesIn, nil +} diff --git a/x/gamm/pool-models/balancer/pool_params.go b/x/gamm/pool-models/balancer/pool_params.go new file mode 100644 index 00000000000..95557298020 --- /dev/null +++ b/x/gamm/pool-models/balancer/pool_params.go @@ -0,0 +1,78 @@ +package balancer + +import ( + "errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v10/x/gamm/types" +) + +func NewPoolParams(swapFee, exitFee sdk.Dec, params *SmoothWeightChangeParams) PoolParams { + return PoolParams{ + SwapFee: swapFee, + ExitFee: exitFee, + SmoothWeightChangeParams: params, + } +} + +func (params PoolParams) Validate(poolWeights []PoolAsset) error { + if params.ExitFee.IsNegative() { + return types.ErrNegativeExitFee + } + + if params.ExitFee.GTE(sdk.OneDec()) { + return types.ErrTooMuchExitFee + } + + if params.SwapFee.IsNegative() { + return types.ErrNegativeSwapFee + } + + if params.SwapFee.GTE(sdk.OneDec()) { + return types.ErrTooMuchSwapFee + } + + if params.SmoothWeightChangeParams != nil { + targetWeights := params.SmoothWeightChangeParams.TargetPoolWeights + // Ensure it has the right number of weights + if len(targetWeights) != len(poolWeights) { + return types.ErrPoolParamsInvalidNumDenoms + } + // Validate all user specified weights + for _, v := range targetWeights { + err := ValidateUserSpecifiedWeight(v.Weight) + if err != nil { + return err + } + } + // Ensure that all the target weight denoms are same as pool asset weights + sortedTargetPoolWeights := SortPoolAssetsOutOfPlaceByDenom(targetWeights) + sortedPoolWeights := SortPoolAssetsOutOfPlaceByDenom(poolWeights) + for i, v := range sortedPoolWeights { + if sortedTargetPoolWeights[i].Token.Denom != v.Token.Denom { + return types.ErrPoolParamsInvalidDenom + } + } + + // No start time validation needed + + // We do not need to validate InitialPoolWeights, as we set that ourselves + // in setInitialPoolParams + + // TODO: Is there anything else we can validate for duration? + if params.SmoothWeightChangeParams.Duration <= 0 { + return errors.New("params.SmoothWeightChangeParams must have a positive duration") + } + } + + return nil +} + +func (params PoolParams) GetPoolSwapFee() sdk.Dec { + return params.SwapFee +} + +func (params PoolParams) GetPoolExitFee() sdk.Dec { + return params.ExitFee +} diff --git a/x/gamm/pool-models/balancer/amm_joinpool_test.go b/x/gamm/pool-models/balancer/pool_suite_test.go similarity index 63% rename from x/gamm/pool-models/balancer/amm_joinpool_test.go rename to x/gamm/pool-models/balancer/pool_suite_test.go index f3dbbe51914..fe861333027 100644 --- a/x/gamm/pool-models/balancer/amm_joinpool_test.go +++ b/x/gamm/pool-models/balancer/pool_suite_test.go @@ -1,8 +1,7 @@ package balancer_test import ( - "errors" - "fmt" + fmt "fmt" "math/rand" "testing" time "time" @@ -10,8 +9,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" - "github.com/osmosis-labs/osmosis/v10/osmoutils" + "github.com/osmosis-labs/osmosis/v10/app/apptesting" + v10 "github.com/osmosis-labs/osmosis/v10/app/upgrades/v10" "github.com/osmosis-labs/osmosis/v10/x/gamm/pool-models/balancer" "github.com/osmosis-labs/osmosis/v10/x/gamm/types" ) @@ -422,6 +423,112 @@ var calcSingleAssetJoinTestCases = []calcJoinSharesTestCase{ }, } +type KeeperTestSuite struct { + apptesting.KeeperTestHelper + + queryClient types.QueryClient +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + +func (suite *KeeperTestSuite) SetupTest() { + suite.Setup() + suite.queryClient = types.NewQueryClient(suite.QueryHelper) + // be post-bug + suite.Ctx = suite.Ctx.WithBlockHeight(v10.ForkHeight) +} + +// This test sets up 2 asset pools, and then checks the spot price on them. +// It uses the pools spot price method, rather than the Gamm keepers spot price method. +func (suite *KeeperTestSuite) TestBalancerSpotPrice() { + baseDenom := "uosmo" + quoteDenom := "uion" + + tests := []struct { + name string + baseDenomPoolInput sdk.Coin + quoteDenomPoolInput sdk.Coin + expectError bool + expectedOutput sdk.Dec + }{ + { + name: "equal value", + baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 100), + quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 100), + expectError: false, + expectedOutput: sdk.MustNewDecFromStr("1"), + }, + { + name: "1:2 ratio", + baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 100), + quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 200), + expectError: false, + expectedOutput: sdk.MustNewDecFromStr("0.500000000000000000"), + }, + { + name: "2:1 ratio", + baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 200), + quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 100), + expectError: false, + expectedOutput: sdk.MustNewDecFromStr("2.000000000000000000"), + }, + { + name: "rounding after sigfig ratio", + baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 220), + quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 115), + expectError: false, + expectedOutput: sdk.MustNewDecFromStr("1.913043480000000000"), // ans is 1.913043478260869565, rounded is 1.91304348 + }, + { + name: "check number of sig figs", + baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 100), + quoteDenomPoolInput: sdk.NewInt64Coin(quoteDenom, 300), + expectError: false, + expectedOutput: sdk.MustNewDecFromStr("0.333333330000000000"), + }, + { + name: "check number of sig figs high sizes", + baseDenomPoolInput: sdk.NewInt64Coin(baseDenom, 343569192534), + quoteDenomPoolInput: sdk.NewCoin(quoteDenom, sdk.MustNewDecFromStr("186633424395479094888742").TruncateInt()), + expectError: false, + expectedOutput: sdk.MustNewDecFromStr("0.000000000001840877"), + }, + } + + for _, tc := range tests { + suite.SetupTest() + + poolId := suite.PrepareUni2PoolWithAssets( + tc.baseDenomPoolInput, + tc.quoteDenomPoolInput, + ) + + pool, err := suite.App.GAMMKeeper.GetPoolAndPoke(suite.Ctx, poolId) + suite.Require().NoError(err, "test: %s", tc.name) + balancerPool, isPool := pool.(*balancer.Pool) + suite.Require().True(isPool, "test: %s", tc.name) + + sut := func() { + spotPrice, err := balancerPool.SpotPrice( + suite.Ctx, + tc.baseDenomPoolInput.Denom, + tc.quoteDenomPoolInput.Denom) + + if tc.expectError { + suite.Require().Error(err, "test: %s", tc.name) + } else { + suite.Require().NoError(err, "test: %s", tc.name) + suite.Require().True(spotPrice.Equal(tc.expectedOutput), + "test: %s\nSpot price wrong, got %s, expected %s\n", tc.name, + spotPrice, tc.expectedOutput) + } + } + assertPoolStateNotModified(suite.T(), balancerPool, sut) + } +} + func (suite *KeeperTestSuite) TestCalcJoinPoolShares() { // We append shared calcSingleAssetJoinTestCases with multi-asset and edge // test cases. @@ -605,7 +712,7 @@ func (suite *KeeperTestSuite) TestCalcJoinPoolShares() { } else { require.NoError(t, err) assertExpectedSharesErrRatio(t, tc.expectShares, shares) - assertExpectedLiquidity(t, tc.expectLiq, tc.tokensIn, liquidity) + assertExpectedLiquidity(t, tc.tokensIn, liquidity) } } @@ -618,499 +725,6 @@ func (suite *KeeperTestSuite) TestCalcJoinPoolShares() { } } -// TestUpdateIntermediaryPoolAssetsLiquidity tests if `updateIntermediaryPoolAssetsLiquidity` returns poolAssetsByDenom map -// with the updated liquidity given by the parameter -func TestUpdateIntermediaryPoolAssetsLiquidity(t *testing.T) { - testCases := []struct { - name string - - // returns newLiquidity, originalPoolAssetsByDenom, expectedPoolAssetsByDenom - setup func() (sdk.Coins, map[string]balancer.PoolAsset, map[string]balancer.PoolAsset) - - err error - }{ - { - name: "regular case with multiple pool assets and a subset of newLiquidity to update", - - setup: func() (sdk.Coins, map[string]balancer.PoolAsset, map[string]balancer.PoolAsset) { - const ( - uosmoValueOriginal = 1_000_000_000_000 - atomValueOriginal = 123 - ionValueOriginal = 657 - - // Weight does not affect calculations so it is shared - weight = 100 - ) - - newLiquidity := sdk.NewCoins( - sdk.NewInt64Coin("uosmo", 1_000), - sdk.NewInt64Coin("atom", 2_000), - sdk.NewInt64Coin("ion", 3_000)) - - originalPoolAssetsByDenom := map[string]balancer.PoolAsset{ - "uosmo": { - Token: sdk.NewInt64Coin("uosmo", uosmoValueOriginal), - Weight: sdk.NewInt(weight), - }, - "atom": { - Token: sdk.NewInt64Coin("atom", atomValueOriginal), - Weight: sdk.NewInt(weight), - }, - "ion": { - Token: sdk.NewInt64Coin("ion", ionValueOriginal), - Weight: sdk.NewInt(weight), - }, - } - - expectedPoolAssetsByDenom := map[string]balancer.PoolAsset{} - for k, v := range originalPoolAssetsByDenom { - expectedValue := balancer.PoolAsset{Token: v.Token, Weight: v.Weight} - expectedValue.Token.Amount = expectedValue.Token.Amount.Add(newLiquidity.AmountOf(k)) - expectedPoolAssetsByDenom[k] = expectedValue - } - - return newLiquidity, originalPoolAssetsByDenom, expectedPoolAssetsByDenom - }, - }, - { - name: "new liquidity has no coins", - - setup: func() (sdk.Coins, map[string]balancer.PoolAsset, map[string]balancer.PoolAsset) { - const ( - uosmoValueOriginal = 1_000_000_000_000 - atomValueOriginal = 123 - ionValueOriginal = 657 - - // Weight does not affect calculations so it is shared - weight = 100 - ) - - newLiquidity := sdk.NewCoins() - - originalPoolAssetsByDenom := map[string]balancer.PoolAsset{ - "uosmo": { - Token: sdk.NewInt64Coin("uosmo", uosmoValueOriginal), - Weight: sdk.NewInt(weight), - }, - "atom": { - Token: sdk.NewInt64Coin("atom", atomValueOriginal), - Weight: sdk.NewInt(weight), - }, - "ion": { - Token: sdk.NewInt64Coin("ion", ionValueOriginal), - Weight: sdk.NewInt(weight), - }, - } - - return newLiquidity, originalPoolAssetsByDenom, originalPoolAssetsByDenom - }, - }, - { - name: "newLiquidity has a coin that poolAssets don't", - - setup: func() (sdk.Coins, map[string]balancer.PoolAsset, map[string]balancer.PoolAsset) { - const ( - uosmoValueOriginal = 1_000_000_000_000 - - // Weight does not affect calculations so it is shared - weight = 100 - ) - - newLiquidity := sdk.NewCoins( - sdk.NewInt64Coin("juno", 1_000)) - - originalPoolAssetsByDenom := map[string]balancer.PoolAsset{ - "uosmo": { - Token: sdk.NewInt64Coin("uosmo", uosmoValueOriginal), - Weight: sdk.NewInt(weight), - }, - } - - return newLiquidity, originalPoolAssetsByDenom, originalPoolAssetsByDenom - }, - - err: fmt.Errorf(balancer.ErrMsgFormatFailedInterimLiquidityUpdate, "juno"), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - newLiquidity, originalPoolAssetsByDenom, expectedPoolAssetsByDenom := tc.setup() - - err := balancer.UpdateIntermediaryPoolAssetsLiquidity(newLiquidity, originalPoolAssetsByDenom) - - require.Equal(t, tc.err, err) - - if tc.err != nil { - return - } - - require.Equal(t, expectedPoolAssetsByDenom, originalPoolAssetsByDenom) - }) - } -} - -func TestCalcSingleAssetJoin(t *testing.T) { - for _, tc := range calcSingleAssetJoinTestCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - pool := createTestPool(t, tc.swapFee, sdk.MustNewDecFromStr("0"), tc.poolAssets...) - - balancerPool, ok := pool.(*balancer.Pool) - require.True(t, ok) - - tokenIn := tc.tokensIn[0] - - poolAssetInDenom := tokenIn.Denom - // when testing a case with tokenIn that does not exist in pool, we just want - // to provide any pool asset. - if tc.expErr != nil && errors.Is(tc.expErr, types.ErrDenomNotFoundInPool) { - poolAssetInDenom = tc.poolAssets[0].Token.Denom - } - - // find pool asset in pool - // must be in pool since weights get scaled in Balancer pool - // constructor - poolAssetIn, err := balancerPool.GetPoolAsset(poolAssetInDenom) - require.NoError(t, err) - - // system under test - sut := func() { - shares, err := balancerPool.CalcSingleAssetJoin(tokenIn, tc.swapFee, poolAssetIn, pool.GetTotalShares()) - - if tc.expErr != nil { - require.Error(t, err) - require.ErrorAs(t, tc.expErr, &err) - require.Equal(t, sdk.ZeroInt(), shares) - return - } - - require.NoError(t, err) - assertExpectedSharesErrRatio(t, tc.expectShares, shares) - } - - if tc.expectPanic { - require.Panics(t, sut) - } else { - require.NotPanics(t, sut) - } - }) - } -} - -func TestCalcJoinSingleAssetTokensIn(t *testing.T) { - testCases := []struct { - name string - swapFee sdk.Dec - poolAssets []balancer.PoolAsset - tokensIn sdk.Coins - expectShares sdk.Int - expectLiqudity sdk.Coins - expErr error - }{ - { - // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) on page 10: - // P_issued = P_supply * ((1 + (A_t / B_t))^W_t - 1) - // - // 2_499_999_968_750 = 1e20 * (( 1 + (50,000 / 1e12))^0.5 - 1) - // - // where: - // P_supply = initial pool supply = 1e20 - // A_t = amount of deposited asset = 50,000 - // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 - // W_t = normalized weight of deposited asset in pool = 0.5 (equally weighted two-asset pool) - // Plugging all of this in, we get: - // Full solution: https://www.wolframalpha.com/input?i=100000000000000000000*%28%281+%2B+%2850000%2F1000000000000%29%29%5E0.5+-+1%29 - // Simplified: P_issued = 2,499,999,968,750 - name: "one token in - equal weights with zero swap fee", - swapFee: sdk.MustNewDecFromStr("0"), - poolAssets: oneTrillionEvenPoolAssets, - tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000)), - expectShares: sdk.NewInt(2_499_999_968_750), - }, - { - // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) on page 10: - // P_issued = P_supply * ((1 + (A_t / B_t))^W_t - 1) - // - // 2_499_999_968_750 = 1e20 * (( 1 + (50,000 / 1e12))^0.5 - 1) - // - // where: - // P_supply = initial pool supply = 1e20 - // A_t = amount of deposited asset = 50,000 - // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 - // W_t = normalized weight of deposited asset in pool = 0.5 (equally weighted two-asset pool) - // Plugging all of this in, we get: - // Full solution: https://www.wolframalpha.com/input?i=100000000000000000000*%28%281+%2B+%2850000%2F1000000000000%29%29%5E0.5+-+1%29 - // Simplified: P_issued = 2,499,999,968,750 - name: "two tokens in - equal weights with zero swap fee", - swapFee: sdk.MustNewDecFromStr("0"), - poolAssets: oneTrillionEvenPoolAssets, - tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000), sdk.NewInt64Coin("uatom", 50_000)), - expectShares: sdk.NewInt(2_499_999_968_750 * 2), - }, - { - // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) with on page 10 - // with swapFeeRatio added: - // P_issued = P_supply * ((1 + (A_t * swapFeeRatio / B_t))^W_t - 1) - // - // 2_487_500_000_000 = 1e20 * (( 1 + (50,000 * (1 - (1 - 0.5) * 0.01) / 1e12))^0.5 - 1) - // - // where: - // P_supply = initial pool supply = 1e20 - // A_t = amount of deposited asset = 50,000 - // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 - // W_t = normalized weight of deposited asset in pool = 0.5 (equally weighted two-asset pool) - // swapFeeRatio = (1 - (1 - W_t) * swapFee) - // Plugging all of this in, we get: - // Full solution: https://www.wolframalpha.com/input?i=100+*10%5E18*%28%281+%2B+%2850000*%281+-+%281-0.5%29+*+0.01%29%2F1000000000000%29%29%5E0.5+-+1%29 - // Simplified: P_issued = 2_487_500_000_000 - name: "one token in - equal weights with swap fee of 0.01", - swapFee: sdk.MustNewDecFromStr("0.01"), - poolAssets: oneTrillionEvenPoolAssets, - tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000)), - expectShares: sdk.NewInt(2_487_500_000_000), - }, - { - // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) with on page 10 - // with swapFeeRatio added: - // P_issued = P_supply * ((1 + (A_t * swapFeeRatio / B_t))^W_t - 1) - // - // 2_487_500_000_000 = 1e20 * (( 1 + (50,000 * (1 - (1 - 0.5) * 0.01) / 1e12))^0.5 - 1) - // - // where: - // P_supply = initial pool supply = 1e20 - // A_t = amount of deposited asset = 50,000 - // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 - // W_t = normalized weight of deposited asset in pool = 0.5 (equally weighted two-asset pool) - // swapFeeRatio = (1 - (1 - W_t) * swapFee) - // Plugging all of this in, we get: - // Full solution: https://www.wolframalpha.com/input?i=100+*10%5E18*%28%281+%2B+%2850000*%281+-+%281-0.5%29+*+0.01%29%2F1000000000000%29%29%5E0.5+-+1%29 - // Simplified: P_issued = 2_487_500_000_000 - name: "two tokens in - equal weights with swap fee of 0.01", - swapFee: sdk.MustNewDecFromStr("0.01"), - poolAssets: oneTrillionEvenPoolAssets, - tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000), sdk.NewInt64Coin("uatom", 50_000)), - expectShares: sdk.NewInt(2_487_500_000_000 * 2), - }, - { - // For uosmo: - // - // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) with on page 10 - // with swapFeeRatio added: - // P_issued = P_supply * ((1 + (A_t * swapFeeRatio / B_t))^W_t - 1) - // - // 2_072_912_400_000_000 = 1e20 * (( 1 + (50,000 * (1 - (1 - 0.83) * 0.03) / 2_000_000_000))^0.83 - 1) - // - // where: - // P_supply = initial pool supply = 1e20 - // A_t = amount of deposited asset = 50,000 - // B_t = existing balance of deposited asset in the pool prior to deposit = 2_000_000_000 - // W_t = normalized weight of deposited asset in pool = 500 / 500 + 100 = 0.83 - // swapFeeRatio = (1 - (1 - W_t) * swapFee) - // Plugging all of this in, we get: - // Full solution: https://www.wolframalpha.com/input?i=100+*10%5E18*%28%281+%2B+%2850000*%281+-+%281-%28500+%2F+%28500+%2B+100%29%29%29+*+0.03%29%2F2000000000%29%29%5E%28500+%2F+%28500+%2B+100%29%29+-+1%29 - // Simplified: P_issued = 2_072_912_400_000_000 - // - // - // For uatom: - // - // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) with on page 10 - // with swapFeeRatio added: - // P_issued = P_supply * ((1 + (A_t * swapFeeRatio / B_t))^W_t - 1) - // - // 1_624_999_900_000 = 1e20 * (( 1 + (100_000 * (1 - (1 - 0.167) * 0.03) / 1e12))^0.167 - 1) - // - // where: - // P_supply = initial pool supply = 1e20 - // A_t = amount of deposited asset = 50,000 - // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 - // W_t = normalized weight of deposited asset in pool = 100 / 500 + 100 = 0.167 - // swapFeeRatio = (1 - (1 - W_t) * swapFee) - // Plugging all of this in, we get: - // Full solution: https://www.wolframalpha.com/input?i=100+*10%5E18*%28%281+%2B+%28100000*%281+-+%281-%28100+%2F+%28500+%2B+100%29%29%29+*+0.03%29%2F1000000000000%29%29%5E%28100+%2F+%28500+%2B+100%29%29+-+1%29 - // Simplified: P_issued = 1_624_999_900_000 - name: "two varying tokens in, varying weights, with swap fee of 0.03", - swapFee: sdk.MustNewDecFromStr("0.03"), - poolAssets: []balancer.PoolAsset{ - { - Token: sdk.NewInt64Coin("uosmo", 2_000_000_000), - Weight: sdk.NewInt(500), - }, - { - Token: sdk.NewInt64Coin("uatom", 1e12), - Weight: sdk.NewInt(100), - }, - }, - tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000), sdk.NewInt64Coin("uatom", 100_000)), - expectShares: sdk.NewInt(2_072_912_400_000_000 + 1_624_999_900_000), - }, - { - name: "no tokens in", - swapFee: sdk.MustNewDecFromStr("0.03"), - poolAssets: oneTrillionEvenPoolAssets, - tokensIn: sdk.NewCoins(), - expectShares: sdk.NewInt(0), - }, - { - name: "one of the tokensIn asset does not exist in pool", - swapFee: sdk.ZeroDec(), - poolAssets: oneTrillionEvenPoolAssets, - // Second tokenIn does not exist. - tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000), sdk.NewInt64Coin(doesNotExistDenom, 50_000)), - expectShares: sdk.ZeroInt(), - expErr: fmt.Errorf(balancer.ErrMsgFormatNoPoolAssetFound, doesNotExistDenom), - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - pool := createTestPool(t, tc.swapFee, sdk.ZeroDec(), tc.poolAssets...) - - balancerPool, ok := pool.(*balancer.Pool) - require.True(t, ok) - - poolAssetsByDenom, err := balancer.GetPoolAssetsByDenom(balancerPool.GetAllPoolAssets()) - require.NoError(t, err) - - // estimate expected liquidity - expectedNewLiquidity := sdk.NewCoins() - for _, tokenIn := range tc.tokensIn { - expectedNewLiquidity = expectedNewLiquidity.Add(tokenIn) - } - - totalNumShares, totalNewLiquidity, err := balancerPool.CalcJoinSingleAssetTokensIn(tc.tokensIn, pool.GetTotalShares(), poolAssetsByDenom, tc.swapFee) - - if tc.expErr != nil { - require.Error(t, err) - require.ErrorAs(t, tc.expErr, &err) - require.Equal(t, sdk.ZeroInt(), totalNumShares) - require.Equal(t, sdk.Coins{}, totalNewLiquidity) - return - } - - require.NoError(t, err) - - require.Equal(t, expectedNewLiquidity, totalNewLiquidity) - - if tc.expectShares.Int64() == 0 { - require.Equal(t, tc.expectShares, totalNumShares) - return - } - - assertExpectedSharesErrRatio(t, tc.expectShares, totalNumShares) - }) - } -} - -// TestGetPoolAssetsByDenom tests if `GetPoolAssetsByDenom` succesfully creates a map of denom to pool asset -// given pool asset as parameter -func TestGetPoolAssetsByDenom(t *testing.T) { - testCases := []struct { - name string - poolAssets []balancer.PoolAsset - expectedPoolAssetsByDenom map[string]balancer.PoolAsset - - err error - }{ - { - name: "zero pool assets", - poolAssets: []balancer.PoolAsset{}, - expectedPoolAssetsByDenom: make(map[string]balancer.PoolAsset), - }, - { - name: "one pool asset", - poolAssets: []balancer.PoolAsset{ - { - Token: sdk.NewInt64Coin("uosmo", 1e12), - Weight: sdk.NewInt(100), - }, - }, - expectedPoolAssetsByDenom: map[string]balancer.PoolAsset{ - "uosmo": { - Token: sdk.NewInt64Coin("uosmo", 1e12), - Weight: sdk.NewInt(100), - }, - }, - }, - { - name: "two pool assets", - poolAssets: []balancer.PoolAsset{ - { - Token: sdk.NewInt64Coin("uosmo", 1e12), - Weight: sdk.NewInt(100), - }, - { - Token: sdk.NewInt64Coin("atom", 123), - Weight: sdk.NewInt(400), - }, - }, - expectedPoolAssetsByDenom: map[string]balancer.PoolAsset{ - "uosmo": { - Token: sdk.NewInt64Coin("uosmo", 1e12), - Weight: sdk.NewInt(100), - }, - "atom": { - Token: sdk.NewInt64Coin("atom", 123), - Weight: sdk.NewInt(400), - }, - }, - }, - { - name: "duplicate pool assets", - poolAssets: []balancer.PoolAsset{ - { - Token: sdk.NewInt64Coin("uosmo", 1e12), - Weight: sdk.NewInt(100), - }, - { - Token: sdk.NewInt64Coin("uosmo", 123), - Weight: sdk.NewInt(400), - }, - }, - err: fmt.Errorf(balancer.ErrMsgFormatRepeatingPoolAssetsNotAllowed, "uosmo"), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actualPoolAssetsByDenom, err := balancer.GetPoolAssetsByDenom(tc.poolAssets) - - require.Equal(t, tc.err, err) - - if tc.err != nil { - return - } - - require.Equal(t, tc.expectedPoolAssetsByDenom, actualPoolAssetsByDenom) - }) - } -} - -func assertExpectedSharesErrRatio(t *testing.T, expectedShares, actualShares sdk.Int) { - allowedErrRatioDec, err := sdk.NewDecFromStr(allowedErrRatio) - require.NoError(t, err) - - errTolerance := osmoutils.ErrTolerance{ - MultiplicativeTolerance: allowedErrRatioDec, - } - - require.Equal( - t, - 0, - errTolerance.Compare(expectedShares, actualShares), - fmt.Sprintf("expectedShares: %s, actualShares: %s", expectedShares.String(), actualShares.String())) -} - -func assertExpectedLiquidity(t *testing.T, expectLiq, tokensJoined, liquidity sdk.Coins) { - if len(expectLiq) != 0 { - require.Equal(t, expectLiq, liquidity) - } else { - require.Equal(t, tokensJoined, liquidity) - } -} - // Tests selecting a random amount of coins to LP, and then that ExitPool(JoinPool(tokens)) // preserves the pools number of LP shares, and returns fewer coins to the acter than they started with. func (suite *KeeperTestSuite) TestRandomizedJoinPoolExitPoolInvariants() { diff --git a/x/gamm/pool-models/balancer/pool_test.go b/x/gamm/pool-models/balancer/pool_test.go new file mode 100644 index 00000000000..942805a17c9 --- /dev/null +++ b/x/gamm/pool-models/balancer/pool_test.go @@ -0,0 +1,1243 @@ +package balancer_test + +import ( + "errors" + "fmt" + "testing" + time "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/osmosis-labs/osmosis/v10/osmoutils" + "github.com/osmosis-labs/osmosis/v10/x/gamm/pool-models/balancer" + "github.com/osmosis-labs/osmosis/v10/x/gamm/types" +) + +var ( + defaultSwapFee = sdk.MustNewDecFromStr("0.025") + defaultExitFee = sdk.MustNewDecFromStr("0.025") + defaultPoolId = uint64(10) + defaultBalancerPoolParams = balancer.PoolParams{ + SwapFee: defaultSwapFee, + ExitFee: defaultExitFee, + } + defaultFutureGovernor = "" + defaultCurBlockTime = time.Unix(1618700000, 0) + // + dummyPoolAssets = []balancer.PoolAsset{} + wantErr = true + noErr = false +) + +// TestUpdateIntermediaryPoolAssetsLiquidity tests if `updateIntermediaryPoolAssetsLiquidity` returns poolAssetsByDenom map +// with the updated liquidity given by the parameter +func TestUpdateIntermediaryPoolAssetsLiquidity(t *testing.T) { + testCases := []struct { + name string + + // returns newLiquidity, originalPoolAssetsByDenom, expectedPoolAssetsByDenom + setup func() (sdk.Coins, map[string]balancer.PoolAsset, map[string]balancer.PoolAsset) + + err error + }{ + { + name: "regular case with multiple pool assets and a subset of newLiquidity to update", + + setup: func() (sdk.Coins, map[string]balancer.PoolAsset, map[string]balancer.PoolAsset) { + const ( + uosmoValueOriginal = 1_000_000_000_000 + atomValueOriginal = 123 + ionValueOriginal = 657 + + // Weight does not affect calculations so it is shared + weight = 100 + ) + + newLiquidity := sdk.NewCoins( + sdk.NewInt64Coin("uosmo", 1_000), + sdk.NewInt64Coin("atom", 2_000), + sdk.NewInt64Coin("ion", 3_000)) + + originalPoolAssetsByDenom := map[string]balancer.PoolAsset{ + "uosmo": { + Token: sdk.NewInt64Coin("uosmo", uosmoValueOriginal), + Weight: sdk.NewInt(weight), + }, + "atom": { + Token: sdk.NewInt64Coin("atom", atomValueOriginal), + Weight: sdk.NewInt(weight), + }, + "ion": { + Token: sdk.NewInt64Coin("ion", ionValueOriginal), + Weight: sdk.NewInt(weight), + }, + } + + expectedPoolAssetsByDenom := map[string]balancer.PoolAsset{} + for k, v := range originalPoolAssetsByDenom { + expectedValue := balancer.PoolAsset{Token: v.Token, Weight: v.Weight} + expectedValue.Token.Amount = expectedValue.Token.Amount.Add(newLiquidity.AmountOf(k)) + expectedPoolAssetsByDenom[k] = expectedValue + } + + return newLiquidity, originalPoolAssetsByDenom, expectedPoolAssetsByDenom + }, + }, + { + name: "new liquidity has no coins", + + setup: func() (sdk.Coins, map[string]balancer.PoolAsset, map[string]balancer.PoolAsset) { + const ( + uosmoValueOriginal = 1_000_000_000_000 + atomValueOriginal = 123 + ionValueOriginal = 657 + + // Weight does not affect calculations so it is shared + weight = 100 + ) + + newLiquidity := sdk.NewCoins() + + originalPoolAssetsByDenom := map[string]balancer.PoolAsset{ + "uosmo": { + Token: sdk.NewInt64Coin("uosmo", uosmoValueOriginal), + Weight: sdk.NewInt(weight), + }, + "atom": { + Token: sdk.NewInt64Coin("atom", atomValueOriginal), + Weight: sdk.NewInt(weight), + }, + "ion": { + Token: sdk.NewInt64Coin("ion", ionValueOriginal), + Weight: sdk.NewInt(weight), + }, + } + + return newLiquidity, originalPoolAssetsByDenom, originalPoolAssetsByDenom + }, + }, + { + name: "newLiquidity has a coin that poolAssets don't", + + setup: func() (sdk.Coins, map[string]balancer.PoolAsset, map[string]balancer.PoolAsset) { + const ( + uosmoValueOriginal = 1_000_000_000_000 + + // Weight does not affect calculations so it is shared + weight = 100 + ) + + newLiquidity := sdk.NewCoins( + sdk.NewInt64Coin("juno", 1_000)) + + originalPoolAssetsByDenom := map[string]balancer.PoolAsset{ + "uosmo": { + Token: sdk.NewInt64Coin("uosmo", uosmoValueOriginal), + Weight: sdk.NewInt(weight), + }, + } + + return newLiquidity, originalPoolAssetsByDenom, originalPoolAssetsByDenom + }, + + err: fmt.Errorf(balancer.ErrMsgFormatFailedInterimLiquidityUpdate, "juno"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + newLiquidity, originalPoolAssetsByDenom, expectedPoolAssetsByDenom := tc.setup() + + err := balancer.UpdateIntermediaryPoolAssetsLiquidity(newLiquidity, originalPoolAssetsByDenom) + + require.Equal(t, tc.err, err) + + if tc.err != nil { + return + } + + require.Equal(t, expectedPoolAssetsByDenom, originalPoolAssetsByDenom) + }) + } +} + +func TestCalcSingleAssetJoin(t *testing.T) { + for _, tc := range calcSingleAssetJoinTestCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + pool := createTestPool(t, tc.swapFee, sdk.MustNewDecFromStr("0"), tc.poolAssets...) + + balancerPool, ok := pool.(*balancer.Pool) + require.True(t, ok) + + tokenIn := tc.tokensIn[0] + + poolAssetInDenom := tokenIn.Denom + // when testing a case with tokenIn that does not exist in pool, we just want + // to provide any pool asset. + if tc.expErr != nil && errors.Is(tc.expErr, types.ErrDenomNotFoundInPool) { + poolAssetInDenom = tc.poolAssets[0].Token.Denom + } + + // find pool asset in pool + // must be in pool since weights get scaled in Balancer pool + // constructor + poolAssetIn, err := balancerPool.GetPoolAsset(poolAssetInDenom) + require.NoError(t, err) + + // system under test + sut := func() { + shares, err := balancerPool.CalcSingleAssetJoin(tokenIn, tc.swapFee, poolAssetIn, pool.GetTotalShares()) + + if tc.expErr != nil { + require.Error(t, err) + require.ErrorAs(t, tc.expErr, &err) + require.Equal(t, sdk.ZeroInt(), shares) + return + } + + require.NoError(t, err) + assertExpectedSharesErrRatio(t, tc.expectShares, shares) + } + + assertPoolStateNotModified(t, balancerPool, func() { + assertPanic(t, tc.expectPanic, sut) + }) + }) + } +} + +func TestCalcJoinSingleAssetTokensIn(t *testing.T) { + testCases := []struct { + name string + swapFee sdk.Dec + poolAssets []balancer.PoolAsset + tokensIn sdk.Coins + expectShares sdk.Int + expectLiqudity sdk.Coins + expErr error + }{ + { + // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) on page 10: + // P_issued = P_supply * ((1 + (A_t / B_t))^W_t - 1) + // + // 2_499_999_968_750 = 1e20 * (( 1 + (50,000 / 1e12))^0.5 - 1) + // + // where: + // P_supply = initial pool supply = 1e20 + // A_t = amount of deposited asset = 50,000 + // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 + // W_t = normalized weight of deposited asset in pool = 0.5 (equally weighted two-asset pool) + // Plugging all of this in, we get: + // Full solution: https://www.wolframalpha.com/input?i=100000000000000000000*%28%281+%2B+%2850000%2F1000000000000%29%29%5E0.5+-+1%29 + // Simplified: P_issued = 2,499,999,968,750 + name: "one token in - equal weights with zero swap fee", + swapFee: sdk.MustNewDecFromStr("0"), + poolAssets: oneTrillionEvenPoolAssets, + tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000)), + expectShares: sdk.NewInt(2_499_999_968_750), + }, + { + // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) on page 10: + // P_issued = P_supply * ((1 + (A_t / B_t))^W_t - 1) + // + // 2_499_999_968_750 = 1e20 * (( 1 + (50,000 / 1e12))^0.5 - 1) + // + // where: + // P_supply = initial pool supply = 1e20 + // A_t = amount of deposited asset = 50,000 + // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 + // W_t = normalized weight of deposited asset in pool = 0.5 (equally weighted two-asset pool) + // Plugging all of this in, we get: + // Full solution: https://www.wolframalpha.com/input?i=100000000000000000000*%28%281+%2B+%2850000%2F1000000000000%29%29%5E0.5+-+1%29 + // Simplified: P_issued = 2,499,999,968,750 + name: "two tokens in - equal weights with zero swap fee", + swapFee: sdk.MustNewDecFromStr("0"), + poolAssets: oneTrillionEvenPoolAssets, + tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000), sdk.NewInt64Coin("uatom", 50_000)), + expectShares: sdk.NewInt(2_499_999_968_750 * 2), + }, + { + // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) with on page 10 + // with swapFeeRatio added: + // P_issued = P_supply * ((1 + (A_t * swapFeeRatio / B_t))^W_t - 1) + // + // 2_487_500_000_000 = 1e20 * (( 1 + (50,000 * (1 - (1 - 0.5) * 0.01) / 1e12))^0.5 - 1) + // + // where: + // P_supply = initial pool supply = 1e20 + // A_t = amount of deposited asset = 50,000 + // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 + // W_t = normalized weight of deposited asset in pool = 0.5 (equally weighted two-asset pool) + // swapFeeRatio = (1 - (1 - W_t) * swapFee) + // Plugging all of this in, we get: + // Full solution: https://www.wolframalpha.com/input?i=100+*10%5E18*%28%281+%2B+%2850000*%281+-+%281-0.5%29+*+0.01%29%2F1000000000000%29%29%5E0.5+-+1%29 + // Simplified: P_issued = 2_487_500_000_000 + name: "one token in - equal weights with swap fee of 0.01", + swapFee: sdk.MustNewDecFromStr("0.01"), + poolAssets: oneTrillionEvenPoolAssets, + tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000)), + expectShares: sdk.NewInt(2_487_500_000_000), + }, + { + // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) with on page 10 + // with swapFeeRatio added: + // P_issued = P_supply * ((1 + (A_t * swapFeeRatio / B_t))^W_t - 1) + // + // 2_487_500_000_000 = 1e20 * (( 1 + (50,000 * (1 - (1 - 0.5) * 0.01) / 1e12))^0.5 - 1) + // + // where: + // P_supply = initial pool supply = 1e20 + // A_t = amount of deposited asset = 50,000 + // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 + // W_t = normalized weight of deposited asset in pool = 0.5 (equally weighted two-asset pool) + // swapFeeRatio = (1 - (1 - W_t) * swapFee) + // Plugging all of this in, we get: + // Full solution: https://www.wolframalpha.com/input?i=100+*10%5E18*%28%281+%2B+%2850000*%281+-+%281-0.5%29+*+0.01%29%2F1000000000000%29%29%5E0.5+-+1%29 + // Simplified: P_issued = 2_487_500_000_000 + name: "two tokens in - equal weights with swap fee of 0.01", + swapFee: sdk.MustNewDecFromStr("0.01"), + poolAssets: oneTrillionEvenPoolAssets, + tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000), sdk.NewInt64Coin("uatom", 50_000)), + expectShares: sdk.NewInt(2_487_500_000_000 * 2), + }, + { + // For uosmo: + // + // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) with on page 10 + // with swapFeeRatio added: + // P_issued = P_supply * ((1 + (A_t * swapFeeRatio / B_t))^W_t - 1) + // + // 2_072_912_400_000_000 = 1e20 * (( 1 + (50,000 * (1 - (1 - 0.83) * 0.03) / 2_000_000_000))^0.83 - 1) + // + // where: + // P_supply = initial pool supply = 1e20 + // A_t = amount of deposited asset = 50,000 + // B_t = existing balance of deposited asset in the pool prior to deposit = 2_000_000_000 + // W_t = normalized weight of deposited asset in pool = 500 / 500 + 100 = 0.83 + // swapFeeRatio = (1 - (1 - W_t) * swapFee) + // Plugging all of this in, we get: + // Full solution: https://www.wolframalpha.com/input?i=100+*10%5E18*%28%281+%2B+%2850000*%281+-+%281-%28500+%2F+%28500+%2B+100%29%29%29+*+0.03%29%2F2000000000%29%29%5E%28500+%2F+%28500+%2B+100%29%29+-+1%29 + // Simplified: P_issued = 2_072_912_400_000_000 + // + // + // For uatom: + // + // Expected output from Balancer paper (https://balancer.fi/whitepaper.pdf) using equation (25) with on page 10 + // with swapFeeRatio added: + // P_issued = P_supply * ((1 + (A_t * swapFeeRatio / B_t))^W_t - 1) + // + // 1_624_999_900_000 = 1e20 * (( 1 + (100_000 * (1 - (1 - 0.167) * 0.03) / 1e12))^0.167 - 1) + // + // where: + // P_supply = initial pool supply = 1e20 + // A_t = amount of deposited asset = 50,000 + // B_t = existing balance of deposited asset in the pool prior to deposit = 1,000,000,000,000 + // W_t = normalized weight of deposited asset in pool = 100 / 500 + 100 = 0.167 + // swapFeeRatio = (1 - (1 - W_t) * swapFee) + // Plugging all of this in, we get: + // Full solution: https://www.wolframalpha.com/input?i=100+*10%5E18*%28%281+%2B+%28100000*%281+-+%281-%28100+%2F+%28500+%2B+100%29%29%29+*+0.03%29%2F1000000000000%29%29%5E%28100+%2F+%28500+%2B+100%29%29+-+1%29 + // Simplified: P_issued = 1_624_999_900_000 + name: "two varying tokens in, varying weights, with swap fee of 0.03", + swapFee: sdk.MustNewDecFromStr("0.03"), + poolAssets: []balancer.PoolAsset{ + { + Token: sdk.NewInt64Coin("uosmo", 2_000_000_000), + Weight: sdk.NewInt(500), + }, + { + Token: sdk.NewInt64Coin("uatom", 1e12), + Weight: sdk.NewInt(100), + }, + }, + tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000), sdk.NewInt64Coin("uatom", 100_000)), + expectShares: sdk.NewInt(2_072_912_400_000_000 + 1_624_999_900_000), + }, + { + name: "no tokens in", + swapFee: sdk.MustNewDecFromStr("0.03"), + poolAssets: oneTrillionEvenPoolAssets, + tokensIn: sdk.NewCoins(), + expectShares: sdk.NewInt(0), + }, + { + name: "one of the tokensIn asset does not exist in pool", + swapFee: sdk.ZeroDec(), + poolAssets: oneTrillionEvenPoolAssets, + // Second tokenIn does not exist. + tokensIn: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 50_000), sdk.NewInt64Coin(doesNotExistDenom, 50_000)), + expectShares: sdk.ZeroInt(), + expErr: fmt.Errorf(balancer.ErrMsgFormatNoPoolAssetFound, doesNotExistDenom), + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + pool := createTestPool(t, tc.swapFee, sdk.ZeroDec(), tc.poolAssets...) + + balancerPool, ok := pool.(*balancer.Pool) + require.True(t, ok) + + poolAssetsByDenom, err := balancer.GetPoolAssetsByDenom(balancerPool.GetAllPoolAssets()) + require.NoError(t, err) + + // estimate expected liquidity + expectedNewLiquidity := sdk.NewCoins() + for _, tokenIn := range tc.tokensIn { + expectedNewLiquidity = expectedNewLiquidity.Add(tokenIn) + } + + sut := func() { + totalNumShares, totalNewLiquidity, err := balancerPool.CalcJoinSingleAssetTokensIn(tc.tokensIn, pool.GetTotalShares(), poolAssetsByDenom, tc.swapFee) + + if tc.expErr != nil { + require.Error(t, err) + require.ErrorAs(t, tc.expErr, &err) + require.Equal(t, sdk.ZeroInt(), totalNumShares) + require.Equal(t, sdk.Coins{}, totalNewLiquidity) + return + } + + require.NoError(t, err) + + require.Equal(t, expectedNewLiquidity, totalNewLiquidity) + + if tc.expectShares.Int64() == 0 { + require.Equal(t, tc.expectShares, totalNumShares) + return + } + + assertExpectedSharesErrRatio(t, tc.expectShares, totalNumShares) + } + + assertPoolStateNotModified(t, balancerPool, sut) + }) + } +} + +// TestGetPoolAssetsByDenom tests if `GetPoolAssetsByDenom` succesfully creates a map of denom to pool asset +// given pool asset as parameter +func TestGetPoolAssetsByDenom(t *testing.T) { + testCases := []struct { + name string + poolAssets []balancer.PoolAsset + expectedPoolAssetsByDenom map[string]balancer.PoolAsset + + err error + }{ + { + name: "zero pool assets", + poolAssets: []balancer.PoolAsset{}, + expectedPoolAssetsByDenom: make(map[string]balancer.PoolAsset), + }, + { + name: "one pool asset", + poolAssets: []balancer.PoolAsset{ + { + Token: sdk.NewInt64Coin("uosmo", 1e12), + Weight: sdk.NewInt(100), + }, + }, + expectedPoolAssetsByDenom: map[string]balancer.PoolAsset{ + "uosmo": { + Token: sdk.NewInt64Coin("uosmo", 1e12), + Weight: sdk.NewInt(100), + }, + }, + }, + { + name: "two pool assets", + poolAssets: []balancer.PoolAsset{ + { + Token: sdk.NewInt64Coin("uosmo", 1e12), + Weight: sdk.NewInt(100), + }, + { + Token: sdk.NewInt64Coin("atom", 123), + Weight: sdk.NewInt(400), + }, + }, + expectedPoolAssetsByDenom: map[string]balancer.PoolAsset{ + "uosmo": { + Token: sdk.NewInt64Coin("uosmo", 1e12), + Weight: sdk.NewInt(100), + }, + "atom": { + Token: sdk.NewInt64Coin("atom", 123), + Weight: sdk.NewInt(400), + }, + }, + }, + { + name: "duplicate pool assets", + poolAssets: []balancer.PoolAsset{ + { + Token: sdk.NewInt64Coin("uosmo", 1e12), + Weight: sdk.NewInt(100), + }, + { + Token: sdk.NewInt64Coin("uosmo", 123), + Weight: sdk.NewInt(400), + }, + }, + err: fmt.Errorf(balancer.ErrMsgFormatRepeatingPoolAssetsNotAllowed, "uosmo"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualPoolAssetsByDenom, err := balancer.GetPoolAssetsByDenom(tc.poolAssets) + + require.Equal(t, tc.err, err) + + if tc.err != nil { + return + } + + require.Equal(t, tc.expectedPoolAssetsByDenom, actualPoolAssetsByDenom) + }) + } +} + +// TestCalculateAmountOutAndIn_InverseRelationship tests that the same amount of token is guaranteed upon +// sequential operation of CalcInAmtGivenOut and CalcOutAmtGivenIn. +func TestCalculateAmountOutAndIn_InverseRelationship(t *testing.T) { + type testcase struct { + denomOut string + initialPoolOut int64 + initialWeightOut int64 + initialCalcOut int64 + + denomIn string + initialPoolIn int64 + initialWeightIn int64 + } + + // For every test case in testcases, apply a swap fee in swapFeeCases. + testcases := []testcase{ + { + denomOut: "uosmo", + initialPoolOut: 1_000_000_000_000, + initialWeightOut: 100, + initialCalcOut: 100, + + denomIn: "ion", + initialPoolIn: 1_000_000_000_000, + initialWeightIn: 100, + }, + { + denomOut: "uosmo", + initialPoolOut: 1_000, + initialWeightOut: 100, + initialCalcOut: 100, + + denomIn: "ion", + initialPoolIn: 1_000_000, + initialWeightIn: 100, + }, + { + denomOut: "uosmo", + initialPoolOut: 1_000, + initialWeightOut: 100, + initialCalcOut: 100, + + denomIn: "ion", + initialPoolIn: 1_000_000, + initialWeightIn: 100, + }, + { + denomOut: "uosmo", + initialPoolOut: 1_000, + initialWeightOut: 200, + initialCalcOut: 100, + + denomIn: "ion", + initialPoolIn: 1_000_000, + initialWeightIn: 50, + }, + { + denomOut: "uosmo", + initialPoolOut: 1_000_000, + initialWeightOut: 200, + initialCalcOut: 100000, + + denomIn: "ion", + initialPoolIn: 1_000_000_000, + initialWeightIn: 50, + }, + } + + swapFeeCases := []string{"0", "0.001", "0.1", "0.5", "0.99"} + + getTestCaseName := func(tc testcase, swapFeeCase string) string { + return fmt.Sprintf("tokenOutInitial: %d, tokenInInitial: %d, initialOut: %d, swapFee: %s", + tc.initialPoolOut, + tc.initialPoolIn, + tc.initialCalcOut, + swapFeeCase, + ) + } + + for _, tc := range testcases { + for _, swapFee := range swapFeeCases { + t.Run(getTestCaseName(tc, swapFee), func(t *testing.T) { + ctx := createTestContext(t) + + poolAssetOut := balancer.PoolAsset{ + Token: sdk.NewInt64Coin(tc.denomOut, tc.initialPoolOut), + Weight: sdk.NewInt(tc.initialWeightOut), + } + + poolAssetIn := balancer.PoolAsset{ + Token: sdk.NewInt64Coin(tc.denomIn, tc.initialPoolIn), + Weight: sdk.NewInt(tc.initialWeightIn), + } + + swapFeeDec, err := sdk.NewDecFromStr(swapFee) + require.NoError(t, err) + + exitFeeDec, err := sdk.NewDecFromStr("0") + require.NoError(t, err) + + pool := createTestPool(t, swapFeeDec, exitFeeDec, poolAssetOut, poolAssetIn) + require.NotNil(t, pool) + + initialOut := sdk.NewInt64Coin(poolAssetOut.Token.Denom, tc.initialCalcOut) + initialOutCoins := sdk.NewCoins(initialOut) + + sut := func() { + actualTokenIn, err := pool.CalcInAmtGivenOut(ctx, initialOutCoins, poolAssetIn.Token.Denom, swapFeeDec) + require.NoError(t, err) + + inverseTokenOut, err := pool.CalcOutAmtGivenIn(ctx, sdk.NewCoins(actualTokenIn), poolAssetOut.Token.Denom, swapFeeDec) + require.NoError(t, err) + + require.Equal(t, initialOut.Denom, inverseTokenOut.Denom) + + expected := initialOut.Amount.ToDec() + actual := inverseTokenOut.Amount.ToDec() + + // allow a rounding error of up to 1 for this relation + tol := sdk.NewDec(1) + require.True(osmoutils.DecApproxEq(t, expected, actual, tol)) + } + + balancerPool, ok := pool.(*balancer.Pool) + require.True(t, ok) + + assertPoolStateNotModified(t, balancerPool, sut) + }) + } + } +} + +func TestCalcSingleAssetInAndOut_InverseRelationship(t *testing.T) { + type testcase struct { + initialPoolOut int64 + initialPoolIn int64 + initialWeightOut int64 + tokenOut int64 + initialWeightIn int64 + } + + // For every test case in testcases, apply a swap fee in swapFeeCases. + testcases := []testcase{ + { + initialPoolOut: 1_000_000_000_000, + tokenOut: 100, + initialWeightOut: 100, + initialWeightIn: 100, + }, + { + initialPoolOut: 1_000_000_000_000, + tokenOut: 100, + initialWeightOut: 50, + initialWeightIn: 100, + }, + { + initialPoolOut: 1_000_000_000_000, + tokenOut: 50, + initialWeightOut: 100, + initialWeightIn: 100, + }, + { + initialPoolOut: 1_000_000_000_000, + tokenOut: 100, + initialWeightOut: 100, + initialWeightIn: 50, + }, + { + initialPoolOut: 1_000_000, + tokenOut: 100, + initialWeightOut: 100, + initialWeightIn: 100, + }, + { + initialPoolOut: 2_351_333, + tokenOut: 7, + initialWeightOut: 148, + initialWeightIn: 57, + }, + { + initialPoolOut: 1_000, + tokenOut: 25, + initialWeightOut: 100, + initialWeightIn: 100, + }, + { + initialPoolOut: 1_000, + tokenOut: 26, + initialWeightOut: 100, + initialWeightIn: 100, + }, + } + + swapFeeCases := []string{"0", "0.001", "0.1", "0.5", "0.99"} + + getTestCaseName := func(tc testcase, swapFeeCase string) string { + return fmt.Sprintf("initialPoolOut: %d, initialCalcOut: %d, initialWeightOut: %d, initialWeightIn: %d, swapFee: %s", + tc.initialPoolOut, + tc.tokenOut, + tc.initialWeightOut, + tc.initialWeightIn, + swapFeeCase, + ) + } + + for _, tc := range testcases { + for _, swapFee := range swapFeeCases { + t.Run(getTestCaseName(tc, swapFee), func(t *testing.T) { + swapFeeDec, err := sdk.NewDecFromStr(swapFee) + require.NoError(t, err) + + initialPoolBalanceOut := sdk.NewInt(tc.initialPoolOut) + + initialWeightOut := sdk.NewInt(tc.initialWeightOut) + initialWeightIn := sdk.NewInt(tc.initialWeightIn) + + initialTotalShares := types.InitPoolSharesSupply.ToDec() + initialCalcTokenOut := sdk.NewInt(tc.tokenOut) + + actualSharesOut := balancer.CalcPoolSharesOutGivenSingleAssetIn( + initialPoolBalanceOut.ToDec(), + initialWeightOut.ToDec().Quo(initialWeightOut.Add(initialWeightIn).ToDec()), + initialTotalShares, + initialCalcTokenOut.ToDec(), + swapFeeDec, + ) + + inverseCalcTokenOut := balancer.CalcSingleAssetInGivenPoolSharesOut( + initialPoolBalanceOut.Add(initialCalcTokenOut).ToDec(), + initialWeightOut.ToDec().Quo(initialWeightOut.Add(initialWeightIn).ToDec()), + initialTotalShares.Add(actualSharesOut), + actualSharesOut, + swapFeeDec, + ) + + tol := sdk.NewDec(1) + require.True(osmoutils.DecApproxEq(t, initialCalcTokenOut.ToDec(), inverseCalcTokenOut, tol)) + }) + } + } +} + +// Expected is un-scaled +func testTotalWeight(t *testing.T, expected sdk.Int, pool balancer.Pool) { + scaledExpected := expected.MulRaw(balancer.GuaranteedWeightPrecision) + require.Equal(t, + scaledExpected.String(), + pool.GetTotalWeight().String()) +} + +// TODO: Refactor this into multiple tests +func TestBalancerPoolUpdatePoolAssetBalance(t *testing.T) { + var poolId uint64 = 10 + + initialAssets := []balancer.PoolAsset{ + { + Weight: sdk.NewInt(100), + Token: sdk.NewCoin("test1", sdk.NewInt(50000)), + }, + { + Weight: sdk.NewInt(200), + Token: sdk.NewCoin("test2", sdk.NewInt(50000)), + }, + } + + pacc, err := balancer.NewBalancerPool(poolId, defaultBalancerPoolParams, initialAssets, defaultFutureGovernor, defaultCurBlockTime) + require.NoError(t, err) + + _, err = pacc.GetPoolAsset("unknown") + require.Error(t, err) + _, err = pacc.GetPoolAsset("") + require.Error(t, err) + + testTotalWeight(t, sdk.NewInt(300), pacc) + + // Break abstractions and start reasoning about the underlying internal representation's APIs. + // TODO: This test actually just needs to be refactored to not be doing this, and just + // create a different pool each time. + + err = pacc.SetInitialPoolAssets([]balancer.PoolAsset{{ + Weight: sdk.NewInt(-1), + Token: sdk.NewCoin("negativeWeight", sdk.NewInt(50000)), + }}) + + require.Error(t, err) + + err = pacc.SetInitialPoolAssets([]balancer.PoolAsset{{ + Weight: sdk.NewInt(0), + Token: sdk.NewCoin("zeroWeight", sdk.NewInt(50000)), + }}) + require.Error(t, err) + + err = pacc.UpdatePoolAssetBalance( + sdk.NewCoin("test1", sdk.NewInt(0))) + require.Error(t, err) + + err = pacc.UpdatePoolAssetBalance( + sdk.Coin{Denom: "test1", Amount: sdk.NewInt(-1)}, + ) + require.Error(t, err) + + err = pacc.UpdatePoolAssetBalance( + sdk.NewCoin("test1", sdk.NewInt(1))) + require.NoError(t, err) + + testTotalWeight(t, sdk.NewInt(300), pacc) + + PoolAsset, err := pacc.GetPoolAsset("test1") + require.NoError(t, err) + require.Equal(t, sdk.NewInt(1).String(), PoolAsset.Token.Amount.String()) +} + +func TestBalancerPoolAssetsWeightAndTokenBalance(t *testing.T) { + // TODO: Add more cases + // asset names should be i ascending order, starting from test1 + tests := []struct { + assets []balancer.PoolAsset + shouldErr bool + }{ + // weight 0 + { + []balancer.PoolAsset{ + { + Weight: sdk.NewInt(0), + Token: sdk.NewCoin("test1", sdk.NewInt(50000)), + }, + }, + wantErr, + }, + // negative weight + { + []balancer.PoolAsset{ + { + Weight: sdk.NewInt(-1), + Token: sdk.NewCoin("test1", sdk.NewInt(50000)), + }, + }, + wantErr, + }, + // 0 token amount + { + []balancer.PoolAsset{ + { + Weight: sdk.NewInt(100), + Token: sdk.NewCoin("test1", sdk.NewInt(0)), + }, + }, + wantErr, + }, + // negative token amount + { + []balancer.PoolAsset{ + { + Weight: sdk.NewInt(100), + Token: sdk.Coin{ + Denom: "test1", + Amount: sdk.NewInt(-1), + }, + }, + }, + wantErr, + }, + // total weight 300 + { + []balancer.PoolAsset{ + { + Weight: sdk.NewInt(200), + Token: sdk.NewCoin("test2", sdk.NewInt(50000)), + }, + { + Weight: sdk.NewInt(100), + Token: sdk.NewCoin("test1", sdk.NewInt(10000)), + }, + }, + noErr, + }, + // two of the same token + { + []balancer.PoolAsset{ + { + Weight: sdk.NewInt(200), + Token: sdk.NewCoin("test2", sdk.NewInt(50000)), + }, + { + Weight: sdk.NewInt(300), + Token: sdk.NewCoin("test1", sdk.NewInt(10000)), + }, + { + Weight: sdk.NewInt(100), + Token: sdk.NewCoin("test2", sdk.NewInt(10000)), + }, + }, + wantErr, + }, + // total weight 7300 + { + []balancer.PoolAsset{ + { + Weight: sdk.NewInt(200), + Token: sdk.NewCoin("test2", sdk.NewInt(50000)), + }, + { + Weight: sdk.NewInt(100), + Token: sdk.NewCoin("test1", sdk.NewInt(10000)), + }, + { + Weight: sdk.NewInt(7000), + Token: sdk.NewCoin("test3", sdk.NewInt(10000)), + }, + }, + noErr, + }, + } + + var poolId uint64 = 10 + + for i, tc := range tests { + pacc, err := balancer.NewBalancerPool(poolId, defaultBalancerPoolParams, tc.assets, defaultFutureGovernor, defaultCurBlockTime) + if tc.shouldErr { + require.Error(t, err, "unexpected lack of error, tc %v", i) + } else { + require.NoError(t, err, "unexpected error, tc %v", i) + expectedTotalWeight := sdk.ZeroInt() + for i, asset := range tc.assets { + expectedTotalWeight = expectedTotalWeight.Add(asset.Weight) + + // Ensure pool assets are sorted + require.Equal(t, "test"+fmt.Sprint(i+1), pacc.PoolAssets[i].Token.Denom) + } + testTotalWeight(t, expectedTotalWeight, pacc) + } + } +} + +// TODO: Figure out what parts of this test, if any, make sense. +func TestGetBalancerPoolAssets(t *testing.T) { + // Adds []PoolAssets, one after another + // if the addition doesn't error, adds the weight of the pool assets to a running total, + // and ensures the pool's total weight is equal to the expected. + // This also ensures that the pool assets remain sorted within the pool. + // Furthermore, it ensures that GetPoolAsset succeeds for everything in the pool, + // and fails for things not in it. + denomNotInPool := "xyzCoin" + + assets := []balancer.PoolAsset{ + { + Weight: sdk.NewInt(200), + Token: sdk.NewCoin("test2", sdk.NewInt(50000)), + }, + { + Weight: sdk.NewInt(100), + Token: sdk.NewCoin("test1", sdk.NewInt(10000)), + }, + { + Weight: sdk.NewInt(200), + Token: sdk.NewCoin("test3", sdk.NewInt(50000)), + }, + { + Weight: sdk.NewInt(100), + Token: sdk.NewCoin("test4", sdk.NewInt(10000)), + }, + } + + // TODO: We need way more robust test cases here, and should table drive these cases + pacc, err := balancer.NewBalancerPool(defaultPoolId, defaultBalancerPoolParams, assets, defaultFutureGovernor, defaultCurBlockTime) + require.NoError(t, err) + + // Hardcoded GetPoolAssets tests. + assets, err = pacc.GetPoolAssets("test1", "test2") + require.NoError(t, err) + require.Equal(t, 2, len(assets)) + + assets, err = pacc.GetPoolAssets("test1", "test2", "test3", "test4") + require.NoError(t, err) + require.Equal(t, 4, len(assets)) + + _, err = pacc.GetPoolAssets("test1", "test5") + require.Error(t, err) + _, err = pacc.GetPoolAssets(denomNotInPool) + require.Error(t, err) + + assets, err = pacc.GetPoolAssets() + require.NoError(t, err) + require.Equal(t, 0, len(assets)) +} + +func TestLBPParamsEmptyStartTime(t *testing.T) { + // Test that when the start time is empty, the pool + // sets its start time to be the first start time it is called on + defaultDuration := 100 * time.Second + + initialPoolAssets := []balancer.PoolAsset{ + { + Weight: sdk.NewInt(1), + Token: sdk.NewCoin("asset1", sdk.NewInt(1000)), + }, + { + Weight: sdk.NewInt(1), + Token: sdk.NewCoin("asset2", sdk.NewInt(1000)), + }, + } + + params := balancer.SmoothWeightChangeParams{ + Duration: defaultDuration, + TargetPoolWeights: []balancer.PoolAsset{ + { + Weight: sdk.NewInt(1), + Token: sdk.NewCoin("asset1", sdk.NewInt(0)), + }, + { + Weight: sdk.NewInt(2), + Token: sdk.NewCoin("asset2", sdk.NewInt(0)), + }, + }, + } + + pacc, err := balancer.NewBalancerPool(defaultPoolId, balancer.PoolParams{ + SmoothWeightChangeParams: ¶ms, + SwapFee: defaultSwapFee, + ExitFee: defaultExitFee, + }, initialPoolAssets, defaultFutureGovernor, defaultCurBlockTime) + require.NoError(t, err) + + // Consistency check that SmoothWeightChangeParams params are set + require.NotNil(t, pacc.PoolParams.SmoothWeightChangeParams) + // Ensure that the start time got set + require.Equal(t, pacc.PoolParams.SmoothWeightChangeParams.StartTime, defaultCurBlockTime) +} + +func TestBalancerPoolPokeTokenWeights(t *testing.T) { + // Set default date + defaultStartTime := time.Unix(1618703511, 0) + defaultStartTimeUnix := defaultStartTime.Unix() + defaultDuration := 100 * time.Second + floatGuaranteedPrecision := float64(balancer.GuaranteedWeightPrecision) + + // testCases don't need to be ordered by time. but the blockTime should be + // less than the end time of the SmoothWeightChange. Testing past the end time + // is already handled. + type testCase struct { + blockTime time.Time + expectedWeights []sdk.Int + } + + // Tests how the pool weights get updated via PokeTokenWeights at different block times. + // The framework underneath will automatically add tests for times before the start time, + // at the start time, at the end time, and after the end time. It is up to the test writer to + // test the behavior at times in-between. + tests := []struct { + // We take the initial weights from here + params balancer.SmoothWeightChangeParams + cases []testCase + }{ + { + // 1:1 pool, between asset1 and asset2 + // transitioning to a 1:2 pool + params: balancer.SmoothWeightChangeParams{ + StartTime: defaultStartTime, + Duration: defaultDuration, + InitialPoolWeights: []balancer.PoolAsset{ + { + Weight: sdk.NewInt(1), + Token: sdk.NewCoin("asset1", sdk.NewInt(0)), + }, + { + Weight: sdk.NewInt(1), + Token: sdk.NewCoin("asset2", sdk.NewInt(0)), + }, + }, + TargetPoolWeights: []balancer.PoolAsset{ + { + Weight: sdk.NewInt(1), + Token: sdk.NewCoin("asset1", sdk.NewInt(0)), + }, + { + Weight: sdk.NewInt(2), + Token: sdk.NewCoin("asset2", sdk.NewInt(0)), + }, + }, + }, + cases: []testCase{ + { + // Halfway through at 50 seconds elapsed + blockTime: time.Unix(defaultStartTimeUnix+50, 0), + expectedWeights: []sdk.Int{ + sdk.NewInt(1 * balancer.GuaranteedWeightPrecision), + // Halfway between 1 & 2 + sdk.NewInt(3 * balancer.GuaranteedWeightPrecision / 2), + }, + }, + { + // Quarter way through at 25 seconds elapsed + blockTime: time.Unix(defaultStartTimeUnix+25, 0), + expectedWeights: []sdk.Int{ + sdk.NewInt(1 * balancer.GuaranteedWeightPrecision), + // Quarter way between 1 & 2 = 1.25 + sdk.NewInt(int64(1.25 * floatGuaranteedPrecision)), + }, + }, + }, + }, + { + // 2:2 pool, between asset1 and asset2 + // transitioning to a 4:1 pool + params: balancer.SmoothWeightChangeParams{ + StartTime: defaultStartTime, + Duration: defaultDuration, + InitialPoolWeights: []balancer.PoolAsset{ + { + Weight: sdk.NewInt(2), + Token: sdk.NewCoin("asset1", sdk.NewInt(0)), + }, + { + Weight: sdk.NewInt(2), + Token: sdk.NewCoin("asset2", sdk.NewInt(0)), + }, + }, + TargetPoolWeights: []balancer.PoolAsset{ + { + Weight: sdk.NewInt(4), + Token: sdk.NewCoin("asset1", sdk.NewInt(0)), + }, + { + Weight: sdk.NewInt(1), + Token: sdk.NewCoin("asset2", sdk.NewInt(0)), + }, + }, + }, + cases: []testCase{ + { + // Halfway through at 50 seconds elapsed + blockTime: time.Unix(defaultStartTimeUnix+50, 0), + expectedWeights: []sdk.Int{ + // Halfway between 2 & 4 + sdk.NewInt(6 * balancer.GuaranteedWeightPrecision / 2), + // Halfway between 1 & 2 + sdk.NewInt(3 * balancer.GuaranteedWeightPrecision / 2), + }, + }, + { + // Quarter way through at 25 seconds elapsed + blockTime: time.Unix(defaultStartTimeUnix+25, 0), + expectedWeights: []sdk.Int{ + // Quarter way between 2 & 4 = 2.5 + sdk.NewInt(int64(2.5 * floatGuaranteedPrecision)), + // Quarter way between 2 & 1 = 1.75 + sdk.NewInt(int64(1.75 * floatGuaranteedPrecision)), + }, + }, + }, + }, + } + + // Add test cases at a time before the start, the start, the end, and a time after the end. + addDefaultCases := func(params balancer.SmoothWeightChangeParams, cases []testCase) []testCase { + // Set times one second before the start, and one second after the end + timeBeforeWeightChangeStart := time.Unix(params.StartTime.Unix()-1, 0) + timeAtWeightChangeEnd := params.StartTime.Add(params.Duration) + timeAfterWeightChangeEnd := time.Unix(timeAtWeightChangeEnd.Unix()+1, 0) + initialWeights := make([]sdk.Int, len(params.InitialPoolWeights)) + finalWeights := make([]sdk.Int, len(params.TargetPoolWeights)) + for i, v := range params.InitialPoolWeights { + initialWeights[i] = v.Weight.MulRaw(balancer.GuaranteedWeightPrecision) + } + for i, v := range params.TargetPoolWeights { + // Doesn't need to be scaled, due to this being done already in param initialization, + // and because params is only shallow copied + finalWeights[i] = v.Weight + } + // Set the test cases for times before the start, and the start + updatedCases := []testCase{ + { + blockTime: timeBeforeWeightChangeStart, + expectedWeights: initialWeights, + }, + { + blockTime: params.StartTime, + expectedWeights: initialWeights, + }, + } + // Append the provided cases + updatedCases = append(updatedCases, cases...) + finalCases := []testCase{ + { + blockTime: timeAtWeightChangeEnd, + expectedWeights: finalWeights, + }, + { + blockTime: timeAfterWeightChangeEnd, + expectedWeights: finalWeights, + }, + } + // Append the final cases + updatedCases = append(updatedCases, finalCases...) + return updatedCases + } + + for poolNum, tc := range tests { + paramsCopy := tc.params + // First we create the initial pool assets we will use + initialPoolAssets := make([]balancer.PoolAsset, len(paramsCopy.InitialPoolWeights)) + for i, asset := range paramsCopy.InitialPoolWeights { + assetCopy := balancer.PoolAsset{ + Weight: asset.Weight, + Token: sdk.NewInt64Coin(asset.Token.Denom, 10000), + } + initialPoolAssets[i] = assetCopy + } + // Initialize the pool + pacc, err := balancer.NewBalancerPool(uint64(poolNum), balancer.PoolParams{ + SwapFee: defaultSwapFee, + ExitFee: defaultExitFee, + SmoothWeightChangeParams: &tc.params, + }, initialPoolAssets, defaultFutureGovernor, defaultCurBlockTime) + require.NoError(t, err, "poolNumber %v", poolNum) + + // Consistency check that SmoothWeightChangeParams params are set + require.NotNil(t, pacc.PoolParams.SmoothWeightChangeParams) + + testCases := addDefaultCases(paramsCopy, tc.cases) + for caseNum, testCase := range testCases { + pacc.PokePool(testCase.blockTime) + + totalWeight := sdk.ZeroInt() + + for assetNum, asset := range pacc.GetAllPoolAssets() { + require.Equal(t, testCase.expectedWeights[assetNum], asset.Weight, + "Didn't get the expected weights, poolNumber %v, caseNumber %v, assetNumber %v", + poolNum, caseNum, assetNum) + + totalWeight = totalWeight.Add(asset.Weight) + } + + require.Equal(t, totalWeight, pacc.GetTotalWeight()) + } + // Should have been deleted by the last test case of after PokeTokenWeights pokes past end time. + require.Nil(t, pacc.PoolParams.SmoothWeightChangeParams) + } +} diff --git a/x/gamm/pool-models/balancer/suite_test.go b/x/gamm/pool-models/balancer/suite_test.go deleted file mode 100644 index cbad8d90a13..00000000000 --- a/x/gamm/pool-models/balancer/suite_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package balancer_test - -import ( - "testing" - - "github.com/stretchr/testify/suite" - - "github.com/osmosis-labs/osmosis/v10/app/apptesting" - v10 "github.com/osmosis-labs/osmosis/v10/app/upgrades/v10" - "github.com/osmosis-labs/osmosis/v10/x/gamm/types" -) - -type KeeperTestSuite struct { - apptesting.KeeperTestHelper - - queryClient types.QueryClient -} - -func TestKeeperTestSuite(t *testing.T) { - suite.Run(t, new(KeeperTestSuite)) -} - -func (suite *KeeperTestSuite) SetupTest() { - suite.Setup() - suite.queryClient = types.NewQueryClient(suite.QueryHelper) - // be post-bug - suite.Ctx = suite.Ctx.WithBlockHeight(v10.ForkHeight) -} diff --git a/x/gamm/pool-models/balancer/util_test.go b/x/gamm/pool-models/balancer/util_test.go index 5b34bc07af0..853d06a614e 100644 --- a/x/gamm/pool-models/balancer/util_test.go +++ b/x/gamm/pool-models/balancer/util_test.go @@ -1,6 +1,7 @@ package balancer_test import ( + "fmt" "testing" "time" @@ -11,6 +12,7 @@ import ( tmtypes "github.com/tendermint/tendermint/proto/tendermint/types" dbm "github.com/tendermint/tm-db" + "github.com/osmosis-labs/osmosis/v10/osmoutils" "github.com/osmosis-labs/osmosis/v10/x/gamm/pool-models/balancer" "github.com/osmosis-labs/osmosis/v10/x/gamm/types" ) @@ -36,3 +38,51 @@ func createTestContext(t *testing.T) sdk.Context { return sdk.NewContext(ms, tmtypes.Header{}, false, logger) } + +func assertExpectedSharesErrRatio(t *testing.T, expectedShares, actualShares sdk.Int) { + allowedErrRatioDec, err := sdk.NewDecFromStr(allowedErrRatio) + require.NoError(t, err) + + errTolerance := osmoutils.ErrTolerance{ + MultiplicativeTolerance: allowedErrRatioDec, + } + + require.Equal( + t, + 0, + errTolerance.Compare(expectedShares, actualShares), + fmt.Sprintf("expectedShares: %s, actualShares: %s", expectedShares.String(), actualShares.String())) +} + +func assertExpectedLiquidity(t *testing.T, tokensJoined, liquidity sdk.Coins) { + require.Equal(t, tokensJoined, liquidity) +} + +// assertPoolStateNotModified asserts that sut (system under test) does not modify +// pool state. +func assertPoolStateNotModified(t *testing.T, pool *balancer.Pool, sut func()) { + // We need to make sure that this method does not mutate state. + oldPoolAssets := pool.GetAllPoolAssets() + oldLiquidity := pool.GetTotalPoolLiquidity(sdk.Context{}) + oldShares := pool.GetTotalShares() + + sut() + + newPoolAssets := pool.GetAllPoolAssets() + newLiquidity := pool.GetTotalPoolLiquidity(sdk.Context{}) + newShares := pool.GetTotalShares() + + require.Equal(t, oldPoolAssets, newPoolAssets) + require.Equal(t, oldLiquidity, newLiquidity) + require.Equal(t, oldShares, newShares) +} + +// assertPanic if expectPanic is true, asserts that sut (system under test) +// panics. If expectPanic is false, asserts that sut does not panic. +func assertPanic(t *testing.T, expectPanic bool, sut func()) { + if expectPanic { + require.Panics(t, sut) + } else { + require.NotPanics(t, sut) + } +}