Skip to content

Commit

Permalink
Implement CalcAmountIn for hashflow-v3 (#634)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisngyn authored Dec 5, 2024
1 parent 722109c commit beedb76
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 27 deletions.
99 changes: 94 additions & 5 deletions pkg/liquidity-source/hashflow-v3/pool_simulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
111 changes: 89 additions & 22 deletions pkg/liquidity-source/hashflow-v3/pool_simulator_test.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
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\"}",
}

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) {
Expand All @@ -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"),
},
}
Expand All @@ -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)
Expand All @@ -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()
}

0 comments on commit beedb76

Please sign in to comment.