From dd05943d41c6845a2a20912ea128a9fc184d8d4e Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Mon, 18 Apr 2022 16:22:42 -0500 Subject: [PATCH 1/5] Add boilerplate logic for stableswap swaps and spot price --- x/gamm/pool-models/balancer/amm.go | 6 +- x/gamm/pool-models/stableswap/amm.go | 28 +++++++++ x/gamm/pool-models/stableswap/amm_test.go | 2 - x/gamm/pool-models/stableswap/pool.go | 72 +++++++++++++++++++---- 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/x/gamm/pool-models/balancer/amm.go b/x/gamm/pool-models/balancer/amm.go index 9e3c33de7ea..1bffdbafec8 100644 --- a/x/gamm/pool-models/balancer/amm.go +++ b/x/gamm/pool-models/balancer/amm.go @@ -131,7 +131,11 @@ func (p *Pool) SwapInAmtGivenOut( if err != nil { return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount is zero or negative") } - tokenInCoin, _ := tokenInDecCoin.TruncateDecimal() + tokenInCoin, tokenInDecimal := tokenInDecCoin.TruncateDecimal() + // if tokenInDecimal is non-zero, we add 1 to the tokenInCoin + if tokenInDecimal.Amount.IsPositive() { + tokenInCoin.Amount.AddRaw(1) + } if !tokenInCoin.Amount.IsPositive() { return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") } diff --git a/x/gamm/pool-models/stableswap/amm.go b/x/gamm/pool-models/stableswap/amm.go index e140a1f75dc..1ebc029394c 100644 --- a/x/gamm/pool-models/stableswap/amm.go +++ b/x/gamm/pool-models/stableswap/amm.go @@ -188,3 +188,31 @@ func spotPrice(baseReserve, quoteReserve sdk.Dec) sdk.Dec { // no need to divide by a, since a = 1. return solveCfmm(baseReserve, quoteReserve, a) } + +// returns outAmt as a decimal +func (pa *Pool) calcOutAmtGivenIn(tokenIn sdk.Coin, tokenOutDenom string, swapFee sdk.Dec) (sdk.Dec, error) { + reserves, err := pa.getPoolAmts(tokenIn.Denom, tokenOutDenom) + if err != nil { + return sdk.Dec{}, err + } + tokenInSupply := reserves[0].ToDec() + tokenOutSupply := reserves[1].ToDec() + // We are solving for the amount of token out, hence x = tokenOutSupply, y = tokenInSupply + outAmt := solveCfmm(tokenOutSupply, tokenInSupply, tokenIn.Amount.ToDec()) + return outAmt, nil +} + +// returns inAmt as a decimal +func (pa *Pool) calcInAmtGivenOut(tokenOut sdk.Coin, tokenInDenom string, swapFee sdk.Dec) (sdk.Dec, error) { + reserves, err := pa.getPoolAmts(tokenInDenom, tokenOut.Denom) + if err != nil { + return sdk.Dec{}, err + } + tokenInSupply := reserves[0].ToDec() + tokenOutSupply := reserves[1].ToDec() + // We are solving for the amount of token in, cfmm(x,y) = cfmm(x + x_in, y - y_out) + // x = tokenInSupply, y = tokenOutSupply, yIn = -tokenOutAmount + inAmtRaw := solveCfmm(tokenInSupply, tokenOutSupply, tokenOut.Amount.ToDec().Neg()) + inAmt := inAmtRaw.NegMut() + return inAmt, nil +} diff --git a/x/gamm/pool-models/stableswap/amm_test.go b/x/gamm/pool-models/stableswap/amm_test.go index 26c466d57d9..38c035814ff 100644 --- a/x/gamm/pool-models/stableswap/amm_test.go +++ b/x/gamm/pool-models/stableswap/amm_test.go @@ -33,8 +33,6 @@ func TestCFMMInvariant(t *testing.T) { sdk.NewDec(100), sdk.NewDec(100), sdk.NewDec(1000), - // returns 87.445364416281417284 - // should return 99.84973704262359 }, // { // sdk.NewDec(100000), diff --git a/x/gamm/pool-models/stableswap/pool.go b/x/gamm/pool-models/stableswap/pool.go index 8f4644958e5..d7e64306bf5 100644 --- a/x/gamm/pool-models/stableswap/pool.go +++ b/x/gamm/pool-models/stableswap/pool.go @@ -7,6 +7,7 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/osmosis-labs/osmosis/v7/x/gamm/types" ) @@ -68,34 +69,85 @@ func (pa Pool) getPoolAmts(denoms ...string) ([]sdk.Int, error) { return result, nil } -// These should all get moved to amm.go +// applySwap updates the pool liquidity. +// It requires caller to validate that tokensIn and tokensOut only consist of +// denominations in the pool. +// The function sanity checks this, and panics if not the case. +func (p *Pool) applySwap(tokensIn sdk.Coins, tokensOut sdk.Coins) { + l := p.PoolLiquidity.Len() + // update liquidity + p.PoolLiquidity = p.PoolLiquidity.Add(tokensIn...).Sub(tokensOut) + // sanity check that no new denoms were added + if len(p.PoolLiquidity) != l { + panic("applySwap changed number of tokens in pool") + } +} + +// TODO: These should all get moved to amm.go func (pa Pool) CalcOutAmtGivenIn(ctx sdk.Context, tokenIn sdk.Coins, tokenOutDenom string, swapFee sdk.Dec) (tokenOut sdk.DecCoin, err error) { if tokenIn.Len() != 1 { - return sdk.DecCoin{}, errors.New("asdf") + return sdk.DecCoin{}, errors.New("stableswap CalcOutAmtGivenIn: tokenIn is of wrong length") } - reserves, err := pa.getPoolAmts(tokenIn[0].Denom, tokenOutDenom) + amt, err := pa.calcOutAmtGivenIn(tokenIn[0], tokenOutDenom, swapFee) if err != nil { return sdk.DecCoin{}, err } - // document which is x vs y - outAmt := solveCfmm(reserves[1].ToDec(), reserves[0].ToDec(), tokenIn[0].Amount.ToDec()) - return sdk.DecCoin{Denom: tokenOutDenom, Amount: outAmt}, nil + return sdk.DecCoin{Denom: tokenOutDenom, Amount: amt}, nil } func (pa *Pool) SwapOutAmtGivenIn(ctx sdk.Context, tokenIn sdk.Coins, tokenOutDenom string, swapFee sdk.Dec) (tokenOut sdk.Coin, err error) { - return sdk.Coin{}, types.ErrNotImplemented + tokenOutDec, err := pa.CalcOutAmtGivenIn(ctx, tokenIn, tokenOutDenom, swapFee) + if err != nil { + return sdk.Coin{}, err + } + // we ignore the decimal component, as token out amount must round down + tokenOut, _ = tokenOutDec.TruncateDecimal() + if !tokenOut.Amount.IsPositive() { + return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") + } + pa.applySwap(tokenIn, sdk.NewCoins(tokenOut)) + + return tokenOut, nil } func (pa Pool) CalcInAmtGivenOut(ctx sdk.Context, tokenOut sdk.Coins, tokenInDenom string, swapFee sdk.Dec) (tokenIn sdk.DecCoin, err error) { - return sdk.DecCoin{}, types.ErrNotImplemented + if tokenOut.Len() != 1 { + return sdk.DecCoin{}, errors.New("stableswap CalcInAmtGivenOut: tokenOut is of wrong length") + } + // TODO: Refactor this later to handle scaling factors + amt, err := pa.calcInAmtGivenOut(tokenOut[0], tokenInDenom, swapFee) + if err != nil { + return sdk.DecCoin{}, err + } + return sdk.DecCoin{Denom: tokenInDenom, Amount: amt}, nil } func (pa *Pool) SwapInAmtGivenOut(ctx sdk.Context, tokenOut sdk.Coins, tokenInDenom string, swapFee sdk.Dec) (tokenIn sdk.Coin, err error) { - return sdk.Coin{}, types.ErrNotImplemented + tokenInDec, err := pa.CalcInAmtGivenOut(ctx, tokenOut, tokenInDenom, swapFee) + if err != nil { + return sdk.Coin{}, err + } + tokenIn, tokenInDecimal := tokenInDec.TruncateDecimal() + // if tokenInDecimal is non-zero, we add 1 to the tokenInCoin + // this is because tokenIn must round up + if tokenInDecimal.Amount.IsPositive() { + tokenIn.Amount.AddRaw(1) + } + if !tokenIn.Amount.IsPositive() { + return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") + } + pa.applySwap(sdk.NewCoins(tokenIn), tokenOut) + + return tokenIn, nil } func (pa Pool) SpotPrice(ctx sdk.Context, baseAssetDenom string, quoteAssetDenom string) (sdk.Dec, error) { - return sdk.Dec{}, types.ErrNotImplemented + reserves, err := pa.getPoolAmts(baseAssetDenom, quoteAssetDenom) + if err != nil { + return sdk.Dec{}, err + } + // TODO: apply scaling factors here + return spotPrice(reserves[0].ToDec(), reserves[1].ToDec()), nil } func (pa Pool) CalcJoinPoolShares(ctx sdk.Context, tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, newLiquidity sdk.Coins, err error) { From 60f4c350fd9e5c3e18022d64c020ae9ffce2e76f Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Mon, 18 Apr 2022 19:11:53 -0500 Subject: [PATCH 2/5] applySwap -> updatePoolLiquidityForSwap --- x/gamm/pool-models/stableswap/pool.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x/gamm/pool-models/stableswap/pool.go b/x/gamm/pool-models/stableswap/pool.go index d7e64306bf5..3b2a6ad7be3 100644 --- a/x/gamm/pool-models/stableswap/pool.go +++ b/x/gamm/pool-models/stableswap/pool.go @@ -69,17 +69,17 @@ func (pa Pool) getPoolAmts(denoms ...string) ([]sdk.Int, error) { return result, nil } -// applySwap updates the pool liquidity. +// updatePoolLiquidityForSwap updates the pool liquidity. // It requires caller to validate that tokensIn and tokensOut only consist of // denominations in the pool. // The function sanity checks this, and panics if not the case. -func (p *Pool) applySwap(tokensIn sdk.Coins, tokensOut sdk.Coins) { +func (p *Pool) updatePoolLiquidityForSwap(tokensIn sdk.Coins, tokensOut sdk.Coins) { l := p.PoolLiquidity.Len() // update liquidity p.PoolLiquidity = p.PoolLiquidity.Add(tokensIn...).Sub(tokensOut) // sanity check that no new denoms were added if len(p.PoolLiquidity) != l { - panic("applySwap changed number of tokens in pool") + panic("updatePoolLiquidityForSwap changed number of tokens in pool") } } @@ -105,7 +105,7 @@ func (pa *Pool) SwapOutAmtGivenIn(ctx sdk.Context, tokenIn sdk.Coins, tokenOutDe if !tokenOut.Amount.IsPositive() { return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") } - pa.applySwap(tokenIn, sdk.NewCoins(tokenOut)) + pa.updatePoolLiquidityForSwap(tokenIn, sdk.NewCoins(tokenOut)) return tokenOut, nil } @@ -136,7 +136,7 @@ func (pa *Pool) SwapInAmtGivenOut(ctx sdk.Context, tokenOut sdk.Coins, tokenInDe if !tokenIn.Amount.IsPositive() { return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") } - pa.applySwap(sdk.NewCoins(tokenIn), tokenOut) + pa.updatePoolLiquidityForSwap(sdk.NewCoins(tokenIn), tokenOut) return tokenIn, nil } From 0b2da2f48e385656cb6b1edd74a75b24a019bc24 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Apr 2022 11:56:27 -0500 Subject: [PATCH 3/5] l -> numTokens --- x/gamm/pool-models/stableswap/pool.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/gamm/pool-models/stableswap/pool.go b/x/gamm/pool-models/stableswap/pool.go index 3b2a6ad7be3..777fccda1eb 100644 --- a/x/gamm/pool-models/stableswap/pool.go +++ b/x/gamm/pool-models/stableswap/pool.go @@ -74,11 +74,11 @@ func (pa Pool) getPoolAmts(denoms ...string) ([]sdk.Int, error) { // denominations in the pool. // The function sanity checks this, and panics if not the case. func (p *Pool) updatePoolLiquidityForSwap(tokensIn sdk.Coins, tokensOut sdk.Coins) { - l := p.PoolLiquidity.Len() + numTokens := p.PoolLiquidity.Len() // update liquidity p.PoolLiquidity = p.PoolLiquidity.Add(tokensIn...).Sub(tokensOut) // sanity check that no new denoms were added - if len(p.PoolLiquidity) != l { + if len(p.PoolLiquidity) != numTokens { panic("updatePoolLiquidityForSwap changed number of tokens in pool") } } From c613b447298c200e6e25a21ef3bb98390618cace Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Mon, 25 Apr 2022 13:47:21 -0500 Subject: [PATCH 4/5] Fix bug matt pointed out --- x/gamm/pool-models/balancer/amm.go | 2 +- x/gamm/pool-models/stableswap/pool.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x/gamm/pool-models/balancer/amm.go b/x/gamm/pool-models/balancer/amm.go index 1bffdbafec8..297d2d85406 100644 --- a/x/gamm/pool-models/balancer/amm.go +++ b/x/gamm/pool-models/balancer/amm.go @@ -134,7 +134,7 @@ func (p *Pool) SwapInAmtGivenOut( tokenInCoin, tokenInDecimal := tokenInDecCoin.TruncateDecimal() // if tokenInDecimal is non-zero, we add 1 to the tokenInCoin if tokenInDecimal.Amount.IsPositive() { - tokenInCoin.Amount.AddRaw(1) + tokenInCoin.Amount = tokenInCoin.Amount.AddRaw(1) } if !tokenInCoin.Amount.IsPositive() { return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") diff --git a/x/gamm/pool-models/stableswap/pool.go b/x/gamm/pool-models/stableswap/pool.go index 777fccda1eb..5a4fcc55c5e 100644 --- a/x/gamm/pool-models/stableswap/pool.go +++ b/x/gamm/pool-models/stableswap/pool.go @@ -131,7 +131,7 @@ func (pa *Pool) SwapInAmtGivenOut(ctx sdk.Context, tokenOut sdk.Coins, tokenInDe // if tokenInDecimal is non-zero, we add 1 to the tokenInCoin // this is because tokenIn must round up if tokenInDecimal.Amount.IsPositive() { - tokenIn.Amount.AddRaw(1) + tokenIn.Amount = tokenIn.Amount.AddRaw(1) } if !tokenIn.Amount.IsPositive() { return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") From 3456a193b21538cc7f81429a825f27e29f303d76 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Mon, 25 Apr 2022 14:48:49 -0500 Subject: [PATCH 5/5] Revert rounding update --- x/gamm/keeper/multihop_test.go | 2 +- x/gamm/pool-models/balancer/amm.go | 12 +++++++----- x/gamm/pool-models/stableswap/pool.go | 13 +++++++------ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/x/gamm/keeper/multihop_test.go b/x/gamm/keeper/multihop_test.go index b7ef8565f25..2b0bfc92069 100644 --- a/x/gamm/keeper/multihop_test.go +++ b/x/gamm/keeper/multihop_test.go @@ -164,7 +164,7 @@ func (suite *KeeperTestSuite) TestBalancerPoolSimpleMultihopSwapExactAmountOut() }() tokenInAmount, err := keeper.MultihopSwapExactAmountOut(suite.ctx, acc1, test.param.routes, test.param.tokenInMaxAmount, test.param.tokenOut) - suite.NoError(err, "test: %v", test.name) + suite.Require().NoError(err, "test: %v", test.name) // Calculate the chained spot price. spotPriceAfter := func() sdk.Dec { diff --git a/x/gamm/pool-models/balancer/amm.go b/x/gamm/pool-models/balancer/amm.go index 297d2d85406..58a2569cced 100644 --- a/x/gamm/pool-models/balancer/amm.go +++ b/x/gamm/pool-models/balancer/amm.go @@ -119,6 +119,11 @@ func (p Pool) CalcInAmtGivenOut( // 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)) + // TODO: Once we make Calc methods return integers + // if tokenInDecimal is non-zero, we add 1 to the tokenInCoin + // if tokenInDecimal.Amount.IsPositive() { + // tokenInCoin.Amount = tokenInCoin.Amount.AddRaw(1) + // } return sdk.NewDecCoinFromDec(tokenInDenom, tokenAmountInBeforeFee), nil } @@ -131,11 +136,8 @@ func (p *Pool) SwapInAmtGivenOut( if err != nil { return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount is zero or negative") } - tokenInCoin, tokenInDecimal := tokenInDecCoin.TruncateDecimal() - // if tokenInDecimal is non-zero, we add 1 to the tokenInCoin - if tokenInDecimal.Amount.IsPositive() { - tokenInCoin.Amount = tokenInCoin.Amount.AddRaw(1) - } + tokenInCoin, _ := tokenInDecCoin.TruncateDecimal() + if !tokenInCoin.Amount.IsPositive() { return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") } diff --git a/x/gamm/pool-models/stableswap/pool.go b/x/gamm/pool-models/stableswap/pool.go index 5a4fcc55c5e..b0f893010b2 100644 --- a/x/gamm/pool-models/stableswap/pool.go +++ b/x/gamm/pool-models/stableswap/pool.go @@ -119,6 +119,12 @@ func (pa Pool) CalcInAmtGivenOut(ctx sdk.Context, tokenOut sdk.Coins, tokenInDen if err != nil { return sdk.DecCoin{}, err } + // TODO: Once we make calc in amt given out return a Coin + // if tokenInDecimal is non-zero, we add 1 to the tokenInCoin + // this is because tokenIn must round up + // if tokenInDecimal.Amount.IsPositive() { + // tokenIn.Amount = tokenIn.Amount.AddRaw(1) + // } return sdk.DecCoin{Denom: tokenInDenom, Amount: amt}, nil } @@ -127,12 +133,7 @@ func (pa *Pool) SwapInAmtGivenOut(ctx sdk.Context, tokenOut sdk.Coins, tokenInDe if err != nil { return sdk.Coin{}, err } - tokenIn, tokenInDecimal := tokenInDec.TruncateDecimal() - // if tokenInDecimal is non-zero, we add 1 to the tokenInCoin - // this is because tokenIn must round up - if tokenInDecimal.Amount.IsPositive() { - tokenIn.Amount = tokenIn.Amount.AddRaw(1) - } + tokenIn, _ = tokenInDec.TruncateDecimal() if !tokenIn.Amount.IsPositive() { return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive") }