diff --git a/pkg/liquidity-source/hashflow-v3/pool_simulator.go b/pkg/liquidity-source/hashflow-v3/pool_simulator.go index 8d8446751..8fbbe9ae1 100644 --- a/pkg/liquidity-source/hashflow-v3/pool_simulator.go +++ b/pkg/liquidity-source/hashflow-v3/pool_simulator.go @@ -15,11 +15,13 @@ import ( ) var ( - ErrEmptyPriceLevels = errors.New("empty price levels") - ErrInsufficientLiquidity = errors.New("insufficient liquidity") - ErrParsingBigFloat = errors.New("invalid float number") - ErrAmountInIsLessThanLowestPriceLevel = errors.New("amountIn is less than lowest price level") - ErrAmountInIsGreaterThanHighestPriceLevel = errors.New("amountIn is greater than highest price level") + ErrEmptyPriceLevels = errors.New("empty price levels") + ErrInsufficientLiquidity = errors.New("insufficient liquidity") + ErrParsingBigFloat = errors.New("invalid float number") + ErrAmountInIsLessThanLowestPriceLevel = errors.New("amountIn is less than lowest price level") + ErrAmountInIsGreaterThanHighestPriceLevel = errors.New("amountIn is greater than highest price level") + ErrAmountOutIsLessThanLowestPriceLevel = errors.New("amountOut is less than lowest price level") + ErrAmountOutIsGreaterThanHighestPriceLevel = errors.New("amountOut is greater than highest price level") ) type ( @@ -149,6 +151,14 @@ func (p *PoolSimulator) CalcAmountOut(params pool.CalcAmountOutParams) (*pool.Ca } } +func (p *PoolSimulator) CalcAmountIn(params pool.CalcAmountInParams) (*pool.CalcAmountInResult, error) { + if params.TokenAmountOut.Token == p.Token1.Address { + return p.swapExactOut(params.TokenAmountOut.Amount, p.Token0, p.Token1, p.ZeroToOnePriceLevels) + } else { + return p.swapExactOut(params.TokenAmountOut.Amount, p.Token1, p.Token0, p.OneToZeroPriceLevels) + } +} + func (p *PoolSimulator) UpdateBalance(params pool.UpdateBalanceParams) { var amountInAfterDecimals, decimalsPow, amountInBF big.Float amountInBF.SetInt(params.TokenAmountIn.Amount) @@ -207,6 +217,43 @@ func (p *PoolSimulator) swap(amountIn *big.Int, baseToken, quoteToken entity.Poo }, nil } +func (p *PoolSimulator) swapExactOut(amountOut *big.Int, baseToken, quoteToken entity.PoolToken, priceLevel []PriceLevel) (*pool.CalcAmountInResult, error) { + var amountOutAfterDecimals, decimalsPow, amountInBF, amountOutBF, priceToleranceBF, amountInToleranceBF big.Float + + amountOutBF.SetInt(amountOut) + decimalsPow.SetFloat64(math.Pow10(int(quoteToken.Decimals))) + amountOutAfterDecimals.Quo(&amountOutBF, &decimalsPow) + + var amountInAfterDecimals big.Float + // Passing amountInAfterDecimals to the function to avoid allocation + err := getAmountIn(&amountOutAfterDecimals, priceLevel, &amountInAfterDecimals) + if err != nil { + return nil, err + } + + decimalsPow.SetFloat64(math.Pow10(int(baseToken.Decimals))) + amountInBF.Mul(&amountInAfterDecimals, &decimalsPow) + + priceToleranceBF.SetFloat64(float64(p.priceTolerance) / float64(priceToleranceBps)) + amountInToleranceBF.Mul(&priceToleranceBF, &amountInBF) + amountInBF.Add(&amountInBF, &amountInToleranceBF) // amountIn = amountIn + tolerance + + amountIn, _ := amountInBF.Int(nil) + + return &pool.CalcAmountInResult{ + TokenAmountIn: &pool.TokenAmount{Token: baseToken.Address, Amount: amountIn}, + Fee: &pool.TokenAmount{Token: baseToken.Address, Amount: bignumber.ZeroBI}, + Gas: p.gas.Quote, + SwapInfo: SwapInfo{ + BaseToken: baseToken.Address, + BaseTokenAmount: amountIn.String(), + QuoteToken: quoteToken.Address, + QuoteTokenAmount: amountOut.String(), + MarketMaker: p.MarketMaker, + }, + }, nil +} + func getAmountOut(amountIn *big.Float, priceLevels []PriceLevel, amountOut *big.Float) error { if len(priceLevels) == 0 { return ErrEmptyPriceLevels @@ -250,6 +297,48 @@ func getAmountOut(amountIn *big.Float, priceLevels []PriceLevel, amountOut *big. return nil } +func getAmountIn(amountOut *big.Float, priceLevels []PriceLevel, amountIn *big.Float) error { + if len(priceLevels) == 0 { + return ErrEmptyPriceLevels + } + + // Check lower bound + if amountOut.Cmp(new(big.Float).Mul(priceLevels[0].Quote, priceLevels[0].Price)) < 0 { + return ErrAmountOutIsLessThanLowestPriceLevel + } + + // Check upper bound + var supportedAmountOut big.Float + for _, priceLevel := range priceLevels { + supportedAmountOut.Add(&supportedAmountOut, new(big.Float).Mul(priceLevel.Quote, priceLevel.Price)) + } + if amountOut.Cmp(&supportedAmountOut) > 0 { + return ErrAmountOutIsGreaterThanHighestPriceLevel + } + + amountLeft := new(big.Float).Set(amountOut) + + for _, priceLevel := range priceLevels { + swappableAmount := new(big.Float).Mul(priceLevel.Quote, priceLevel.Price) + if swappableAmount.Cmp(amountLeft) > 0 { + swappableAmount = new(big.Float).Set(amountLeft) + } + + amountIn.Add(amountIn, new(big.Float).Quo(swappableAmount, priceLevel.Price)) + amountLeft = amountLeft.Sub(amountLeft, swappableAmount) + + if amountLeft.Cmp(zeroBF) == 0 { + break + } + } + + if amountLeft.Cmp(zeroBF) != 0 { + return ErrInsufficientLiquidity // Should not happen + } + + return nil +} + func getNewPriceLevelsState(amountIn *big.Float, priceLevels []PriceLevel) []PriceLevel { if len(priceLevels) == 0 { return priceLevels diff --git a/pkg/liquidity-source/hashflow-v3/pool_simulator_test.go b/pkg/liquidity-source/hashflow-v3/pool_simulator_test.go index c7f521d1e..d0744d534 100644 --- a/pkg/liquidity-source/hashflow-v3/pool_simulator_test.go +++ b/pkg/liquidity-source/hashflow-v3/pool_simulator_test.go @@ -1,23 +1,27 @@ package hashflowv3 import ( + "math" "math/big" "testing" "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" ) +var ( + tokenOMG = &entity.PoolToken{Address: "0xd26114cd6ee289accf82350c8d8487fedb8a0c07", Decimals: 18, Swappable: true} + tokenUSDT = &entity.PoolToken{Address: "0xdac17f958d2ee523a2206206994597c13d831ec7", Decimals: 6, Swappable: true} +) + var entityPool = entity.Pool{ - Address: "hashflow_v3_mm22_0xd26114cd6ee289accf82350c8d8487fedb8a0c07_0xdac17f958d2ee523a2206206994597c13d831ec7", - Exchange: "hashflow-v3", - Type: "hashflow-v3", - Reserves: []string{"64160215600609997156352", "152481964"}, - Tokens: []*entity.PoolToken{ - {Address: "0xd26114cd6ee289accf82350c8d8487fedb8a0c07", Decimals: 18, Swappable: true}, - {Address: "0xdac17f958d2ee523a2206206994597c13d831ec7", Decimals: 6, Swappable: true}, - }, + Address: "hashflow_v3_mm22_0xd26114cd6ee289accf82350c8d8487fedb8a0c07_0xdac17f958d2ee523a2206206994597c13d831ec7", + Exchange: "hashflow-v3", + Type: "hashflow-v3", + Reserves: []string{"64160215600609997156352", "152481964"}, + Tokens: []*entity.PoolToken{tokenOMG, tokenUSDT}, Extra: "{\"zeroToOnePriceLevels\":[{\"q\":\"21.491858434308554\",\"p\":\"0.6924563136573486\"},{\"q\":\"2127.693984996547\",\"p\":\"0.6924563136573486\"},{\"q\":\"6450.785753788268\",\"p\":\"0.695858410957807\"},{\"q\":\"7095.864329167098\",\"p\":\"0.6955119978476955\"},{\"q\":\"7805.450762083805\",\"p\":\"0.6951337575443223\"},{\"q\":\"8588.352341200025\",\"p\":\"0.6945303753566658\"},{\"q\":\"9458.145774233493\",\"p\":\"0.6932765640141211\"},{\"q\":\"10403.960351656831\",\"p\":\"0.6927876203981647\"},{\"q\":\"11466.95813207097\",\"p\":\"0.6908910457830065\"},{\"q\":\"741.5123129786516\",\"p\":\"0.6865341216331126\"}],\"oneToZeroPriceLevels\":[{\"q\":\"1.52481964177280676980070027723634\",\"p\":\"1.414875966391418599745334487109784\"},{\"q\":\"150.957144535507867877909404465650\",\"p\":\"1.414875966391418599745334487109784\"}]}", StaticExtra: "{\"marketMaker\":\"mm22\"}", } @@ -25,15 +29,15 @@ var entityPool = entity.Pool{ func TestPoolSimulator_NewPool(t *testing.T) { poolSimulator, err := NewPoolSimulator(entityPool) assert.NoError(t, err) - assert.Equal(t, "0xd26114cd6ee289accf82350c8d8487fedb8a0c07", poolSimulator.Token0.Address) - assert.Equal(t, "0xdac17f958d2ee523a2206206994597c13d831ec7", poolSimulator.Token1.Address) + assert.Equal(t, tokenOMG.Address, poolSimulator.Token0.Address) + assert.Equal(t, tokenUSDT.Address, poolSimulator.Token1.Address) assert.Equal(t, "mm22", poolSimulator.MarketMaker) assert.NotNil(t, poolSimulator.ZeroToOnePriceLevels) assert.NotNil(t, poolSimulator.OneToZeroPriceLevels) - assert.Equal(t, []string{"0xdac17f958d2ee523a2206206994597c13d831ec7"}, poolSimulator.CanSwapTo("0xd26114cd6ee289accf82350c8d8487fedb8a0c07")) - assert.Equal(t, []string{"0xdac17f958d2ee523a2206206994597c13d831ec7"}, poolSimulator.CanSwapFrom("0xd26114cd6ee289accf82350c8d8487fedb8a0c07")) - assert.Equal(t, []string{"0xd26114cd6ee289accf82350c8d8487fedb8a0c07"}, poolSimulator.CanSwapTo("0xdac17f958d2ee523a2206206994597c13d831ec7")) - assert.Equal(t, []string{"0xd26114cd6ee289accf82350c8d8487fedb8a0c07"}, poolSimulator.CanSwapFrom("0xdac17f958d2ee523a2206206994597c13d831ec7")) + assert.Equal(t, []string{tokenUSDT.Address}, poolSimulator.CanSwapTo(tokenOMG.Address)) + assert.Equal(t, []string{tokenUSDT.Address}, poolSimulator.CanSwapFrom(tokenOMG.Address)) + assert.Equal(t, []string{tokenOMG.Address}, poolSimulator.CanSwapTo(tokenUSDT.Address)) + assert.Equal(t, []string{tokenOMG.Address}, poolSimulator.CanSwapFrom(tokenUSDT.Address)) } func TestPoolSimulator_GetAmountOut(t *testing.T) { @@ -47,23 +51,23 @@ func TestPoolSimulator_GetAmountOut(t *testing.T) { expectedErr error }{ { - name: "it should return error when swap lower than min level", // Lowest level ~1.5 USDC - amountIn: big.NewInt(1_000_000), + name: "it should return error when swap lower than min level", // Lowest level ~1.5 USDT + amountIn: floatToWei(t, 1.0, tokenUSDT.Decimals), expectedErr: ErrAmountInIsLessThanLowestPriceLevel, }, { - name: "it should return error when swap higher than total level", // Total level ~151.5 USDC - amountIn: big.NewInt(200_000_000), + name: "it should return error when swap higher than total level", // Total level ~151.5 USDT + amountIn: floatToWei(t, 200.0, tokenUSDT.Decimals), expectedErr: ErrAmountInIsGreaterThanHighestPriceLevel, }, { name: "it should return correct amountOut when swap in levels", - amountIn: big.NewInt(3_000_000), + amountIn: floatToWei(t, 3.0, tokenUSDT.Decimals), expectedAmountOut: bigIntFromString("4244627899174255799"), }, { name: "it should return correct amountOut when swap in all levels", - amountIn: big.NewInt(152_000_000), + amountIn: floatToWei(t, 152.0, tokenUSDT.Decimals), expectedAmountOut: bigIntFromString("215061146891495627168"), }, } @@ -73,10 +77,10 @@ func TestPoolSimulator_GetAmountOut(t *testing.T) { // Swap one to zero params := pool.CalcAmountOutParams{ TokenAmountIn: pool.TokenAmount{ - Token: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Token: tokenUSDT.Address, Amount: tc.amountIn, }, - TokenOut: "0xd26114cd6ee289accf82350c8d8487fedb8a0c07", + TokenOut: tokenOMG.Address, } result, err := poolSimulator.CalcAmountOut(params) @@ -88,7 +92,70 @@ func TestPoolSimulator_GetAmountOut(t *testing.T) { } } +func TestPoolSimulator_GetAmountIn(t *testing.T) { + poolSimulator, err := NewPoolSimulator(entityPool) + assert.NoError(t, err) + + tests := []struct { + name string + amountOut *big.Int + expectedAmountIn *big.Int + expectedErr error + }{ + { + name: "it should return error when swap lower than min level", // Lowest level ~2.1 OMG + amountOut: floatToWei(t, 2.0, tokenOMG.Decimals), + expectedErr: ErrAmountOutIsLessThanLowestPriceLevel, + }, + { + name: "it should return error when swap higher than total level", // Total level ~214.8 OMG + amountOut: floatToWei(t, 220.0, tokenOMG.Decimals), + expectedErr: ErrAmountOutIsGreaterThanHighestPriceLevel, + }, + { + name: "it should return correct amountIn when swap in levels", + amountOut: bigIntFromString("4244627899174255799"), + expectedAmountIn: bigIntFromString("2999999"), + }, + { + name: "it should return correct amountIn when swap in all levels", + amountOut: bigIntFromString("215061146891495627168"), + expectedAmountIn: bigIntFromString("152000000"), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Swap one to zero + params := pool.CalcAmountInParams{ + TokenAmountOut: pool.TokenAmount{ + Token: tokenOMG.Address, + Amount: tc.amountOut, + }, + TokenIn: tokenUSDT.Address, + } + + result, err := poolSimulator.CalcAmountIn(params) + assert.Equal(t, tc.expectedErr, err) + if tc.expectedErr == nil { + assert.Equal(t, tc.expectedAmountIn, result.TokenAmountIn.Amount) + } + }) + } +} + func bigIntFromString(s string) *big.Int { value, _ := new(big.Int).SetString(s, 10) return value } + +func floatToWei(t *testing.T, amount float64, decimals uint8) *big.Int { + if math.IsNaN(amount) || math.IsInf(amount, 0) { + t.Fatalf("invalid number: %f", amount) + } + + d := decimal.NewFromFloat(amount) + expo := decimal.New(1, int32(decimals)) + + return d.Mul(expo).BigInt() +}