Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add boilerplate logic for stableswap swaps and spot price #1287

Merged
merged 6 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion x/gamm/keeper/multihop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func (suite *KeeperTestSuite) TestBalancerPoolSimpleMultihopSwapExactAmountOut()
}()

tokenInAmount, err := keeper.MultihopSwapExactAmountOut(suite.Ctx, suite.TestAccs[0], 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 {
Expand Down
6 changes: 6 additions & 0 deletions x/gamm/pool-models/balancer/amm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -132,6 +137,7 @@ func (p *Pool) SwapInAmtGivenOut(
return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount is zero or negative")
}
tokenInCoin, _ := tokenInDecCoin.TruncateDecimal()

if !tokenInCoin.Amount.IsPositive() {
return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive")
}
Expand Down
28 changes: 28 additions & 0 deletions x/gamm/pool-models/stableswap/amm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Comment on lines +213 to +215
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain why we use a negative value for y in?

inAmt := inAmtRaw.NegMut()
return inAmt, nil
}
2 changes: 0 additions & 2 deletions x/gamm/pool-models/stableswap/amm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
73 changes: 63 additions & 10 deletions x/gamm/pool-models/stableswap/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -68,34 +69,86 @@ func (pa Pool) getPoolAmts(denoms ...string) ([]sdk.Int, error) {
return result, nil
}

// These should all get moved to amm.go
// 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) updatePoolLiquidityForSwap(tokensIn sdk.Coins, tokensOut sdk.Coins) {
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) != numTokens {
panic("updatePoolLiquidityForSwap changed number of tokens in pool")
}
Comment on lines +81 to +83
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love this! 🚀

}

// 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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it make sense to format the length value in the message?

}
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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it make sense to format what it actually was in the message?

}
pa.updatePoolLiquidityForSwap(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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: format the length if makes sense

}
// TODO: Refactor this later to handle scaling factors
amt, err := pa.calcInAmtGivenOut(tokenOut[0], tokenInDenom, swapFee)
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
}

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, _ = tokenInDec.TruncateDecimal()
if !tokenIn.Amount.IsPositive() {
return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidMathApprox, "token amount must be positive")
}
pa.updatePoolLiquidityForSwap(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) {
Expand Down