From f5377f477809120f4fe9f2916f90be1f3596cd53 Mon Sep 17 00:00:00 2001 From: "le.cao" Date: Wed, 24 Jan 2024 15:52:19 +0700 Subject: [PATCH 1/4] add univ2 InitPoolSimulator to allow caller to re-use sim instance --- .../uniswap-v2/pool_simulator.go | 64 +++++++++++++++++ .../uniswap-v2/pool_simulator_test.go | 68 +++++++++++++++++++ pkg/util/bignumber/bignumber.go | 39 +++++++++++ pkg/util/bignumber/bignumber_test.go | 15 ++++ pkg/util/testutil/bignumber.go | 36 ++++++++++ 5 files changed, 222 insertions(+) create mode 100644 pkg/util/testutil/bignumber.go diff --git a/pkg/liquidity-source/uniswap-v2/pool_simulator.go b/pkg/liquidity-source/uniswap-v2/pool_simulator.go index c084974fd..afb6de5ac 100644 --- a/pkg/liquidity-source/uniswap-v2/pool_simulator.go +++ b/pkg/liquidity-source/uniswap-v2/pool_simulator.go @@ -61,6 +61,70 @@ func NewPoolSimulator(entityPool entity.Pool) (*PoolSimulator, error) { }, nil } +func NewPoolSimulatorV2(entityPool entity.Pool) (*PoolSimulator, error) { + sim := &PoolSimulator{} + err := InitPoolSimulator(entityPool, sim) + if err != nil { + return nil, err + } + return sim, nil +} + +const NUM_TOKEN = 2 + +func InitPoolSimulator(entityPool entity.Pool, sim *PoolSimulator) error { + var extra Extra + if err := json.Unmarshal([]byte(entityPool.Extra), &extra); err != nil { + return err + } + + sim.Pool.Info.Address = entityPool.Address + sim.Pool.Info.ReserveUsd = entityPool.ReserveUsd + sim.Pool.Info.Exchange = entityPool.Exchange + sim.Pool.Info.Type = entityPool.Type + sim.Pool.Info.BlockNumber = entityPool.BlockNumber + sim.gas = defaultGas + + if len(entityPool.Tokens) != NUM_TOKEN || len(entityPool.Reserves) != NUM_TOKEN { + return errors.New("Invalid number of token") + } + + if cap(sim.Pool.Info.Tokens) >= NUM_TOKEN { + sim.Pool.Info.Tokens = sim.Pool.Info.Tokens[:NUM_TOKEN] + } else { + sim.Pool.Info.Tokens = make([]string, NUM_TOKEN) + } + for i := range entityPool.Tokens { + sim.Pool.Info.Tokens[i] = entityPool.Tokens[i].Address + } + + if cap(sim.Pool.Info.Reserves) >= NUM_TOKEN { + sim.Pool.Info.Reserves = sim.Pool.Info.Reserves[:NUM_TOKEN] + } else { + sim.Pool.Info.Reserves = make([]*big.Int, NUM_TOKEN) + } + var tmp uint256.Int + for i := range entityPool.Reserves { + tmp.SetFromDecimal(entityPool.Reserves[i]) + if sim.Pool.Info.Reserves[i] == nil { + sim.Pool.Info.Reserves[i] = new(big.Int) + } + utils.FillBig(&tmp, sim.Pool.Info.Reserves[i]) + } + + if sim.fee == nil { + sim.fee = new(uint256.Int) + } + sim.fee.SetUint64(extra.Fee) + + if sim.feePrecision == nil { + sim.feePrecision = new(uint256.Int) + } + sim.feePrecision.SetUint64(extra.FeePrecision) + + return nil +} + func (s *PoolSimulator) CalcAmountOut(param poolpkg.CalcAmountOutParams) (*poolpkg.CalcAmountOutResult, error) { var ( tokenAmountIn = param.TokenAmountIn diff --git a/pkg/liquidity-source/uniswap-v2/pool_simulator_test.go b/pkg/liquidity-source/uniswap-v2/pool_simulator_test.go index 52d66a24e..dcc6a231d 100644 --- a/pkg/liquidity-source/uniswap-v2/pool_simulator_test.go +++ b/pkg/liquidity-source/uniswap-v2/pool_simulator_test.go @@ -1,14 +1,18 @@ package uniswapv2 import ( + "fmt" "math/big" "testing" + "github.com/goccy/go-json" "github.com/holiman/uint256" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/KyberNetwork/blockchain-toolkit/number" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" poolpkg "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" utils "github.com/KyberNetwork/kyberswap-dex-lib/pkg/util/bignumber" "github.com/KyberNetwork/kyberswap-dex-lib/pkg/util/testutil" @@ -233,3 +237,67 @@ func BenchmarkPoolSimulatorCalcAmountOut(b *testing.B) { }) } } + +var ( + poolRedisBench = `{ + "address": "0x3f3ee751ab00246cb0beec2e904ef51e18ac4d77", + "reserveUsd": 0.036380575233191714, + "amplifiedTvl": 0.036380575233191714, + "exchange": "uniswap", + "type": "uniswap-v2", + "timestamp": 1705851986, + "reserves": ["7535596323597", "8596033702378"], + "tokens": [ + { "address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", "swappable": true }, + { "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "swappable": true } + ], + "extra": "{\"fee\":3,\"feePrecision\":1000}", + "blockNumber": 19056224 + }` + + poolEnt entity.Pool + _ = json.Unmarshal([]byte(poolRedisBench), &poolEnt) +) + +func BenchmarkNewPoolSimulator(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = NewPoolSimulator(poolEnt) + } +} + +func BenchmarkNewPoolSimulatorV2(b *testing.B) { + sim, _ := NewPoolSimulatorV2(poolEnt) + for i := 0; i < b.N; i++ { + _ = InitPoolSimulator(poolEnt, sim) + } +} + +func TestComparePoolSimulatorV2(t *testing.T) { + sim, err := NewPoolSimulator(poolEnt) + require.Nil(t, err) + simV2, err := NewPoolSimulatorV2(poolEnt) + require.Nil(t, err) + + for i := 0; i < 500; i++ { + amt := testutil.RandNumberString(15) + t.Run(fmt.Sprintf("test %s", amt), func(t *testing.T) { + in := poolpkg.CalcAmountOutParams{ + TokenAmountIn: poolpkg.TokenAmount{ + Amount: utils.NewBig(amt), + Token: "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + }, + TokenOut: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + Limit: nil, + } + res, err := sim.CalcAmountOut(in) + resV2, errV2 := simV2.CalcAmountOut(in) + + if err == nil { + require.Nil(t, errV2) + assert.Equal(t, res, resV2) + } else { + require.NotNil(t, errV2) + } + }) + } +} diff --git a/pkg/util/bignumber/bignumber.go b/pkg/util/bignumber/bignumber.go index 28fab867f..d1c33ba06 100644 --- a/pkg/util/bignumber/bignumber.go +++ b/pkg/util/bignumber/bignumber.go @@ -1,10 +1,16 @@ package bignumber import ( + "encoding/hex" "math" "math/big" + "math/bits" + + "github.com/holiman/uint256" ) +const MaxWords = 256 / bits.UintSize + var ( // TwoPow128 2^128 TwoPow128 = new(big.Int).Exp(big.NewInt(2), big.NewInt(128), nil) @@ -16,6 +22,8 @@ var ( Four = big.NewInt(4) Five = big.NewInt(5) Six = big.NewInt(6) + + MaxU256Hex, _ = hex.DecodeString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") ) var BONE = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) @@ -39,3 +47,34 @@ func NewBig(s string) (res *big.Int) { res, _ = new(big.Int).SetString(s, 0) return res } + +// similar to `z.ToBig()` but try to re-use space inside `b` instead of allocating +func FillBig(z *uint256.Int, b *big.Int) { + switch MaxWords { // Compile-time check. + case 4: // 64-bit architectures. + if cap(b.Bits()) < 4 { + // this will resize b, we can be sure that b will only hold at most MaxU256 + b.SetBytes(MaxU256Hex) + } + words := b.Bits()[:4] + words[0] = big.Word(z[0]) + words[1] = big.Word(z[1]) + words[2] = big.Word(z[2]) + words[3] = big.Word(z[3]) + b.SetBits(words) + case 8: // 32-bit architectures. + if cap(b.Bits()) < 8 { + b.SetBytes(MaxU256Hex) + } + words := b.Bits()[:8] + words[0] = big.Word(z[0]) + words[1] = big.Word(z[0] >> 32) + words[2] = big.Word(z[1]) + words[3] = big.Word(z[1] >> 32) + words[4] = big.Word(z[2]) + words[5] = big.Word(z[2] >> 32) + words[6] = big.Word(z[3]) + words[7] = big.Word(z[3] >> 32) + b.SetBits(words) + } +} diff --git a/pkg/util/bignumber/bignumber_test.go b/pkg/util/bignumber/bignumber_test.go index 854001cd5..c624809ea 100644 --- a/pkg/util/bignumber/bignumber_test.go +++ b/pkg/util/bignumber/bignumber_test.go @@ -1,9 +1,12 @@ package bignumber import ( + "fmt" "math/big" "testing" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/util/testutil" + "github.com/holiman/uint256" "github.com/stretchr/testify/assert" ) @@ -30,3 +33,15 @@ func TestTenPowDecimals(t *testing.T) { } } + +func TestFillBig(t *testing.T) { + var bi big.Int + for i := 0; i < 500; i++ { + number := testutil.RandNumberHexString(64) + t.Run(fmt.Sprintf("test %s", number), func(t *testing.T) { + u := uint256.MustFromHex("0x" + number) + FillBig(u, &bi) + assert.Equal(t, u.Dec(), bi.Text(10)) + }) + } +} diff --git a/pkg/util/testutil/bignumber.go b/pkg/util/testutil/bignumber.go new file mode 100644 index 000000000..99b0ab0d2 --- /dev/null +++ b/pkg/util/testutil/bignumber.go @@ -0,0 +1,36 @@ +package testutil + +import ( + "fmt" + "math/rand" +) + +func RandNumberString(maxLen int) string { + sLen := rand.Intn(maxLen) + 1 + var s string + for i := 0; i < sLen; i++ { + var c int + if i == 0 { + c = rand.Intn(9) + 1 + } else { + c = rand.Intn(10) + } + s = fmt.Sprintf("%s%d", s, c) + } + return s +} + +func RandNumberHexString(maxLen int) string { + sLen := rand.Intn(maxLen) + 1 + var s string + for i := 0; i < sLen; i++ { + var c int + if i == 0 { + c = rand.Intn(15) + 1 + } else { + c = rand.Intn(16) + } + s = fmt.Sprintf("%s%x", s, c) + } + return s +} From ae0ad293da9db4d851a0450e426fc7b785bfcca8 Mon Sep 17 00:00:00 2001 From: "le.cao" Date: Wed, 24 Jan 2024 16:01:21 +0700 Subject: [PATCH 2/4] check decimal error --- pkg/liquidity-source/uniswap-v2/pool_simulator.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/liquidity-source/uniswap-v2/pool_simulator.go b/pkg/liquidity-source/uniswap-v2/pool_simulator.go index afb6de5ac..87e3a6eda 100644 --- a/pkg/liquidity-source/uniswap-v2/pool_simulator.go +++ b/pkg/liquidity-source/uniswap-v2/pool_simulator.go @@ -105,7 +105,10 @@ func InitPoolSimulator(entityPool entity.Pool, sim *PoolSimulator) error { } var tmp uint256.Int for i := range entityPool.Reserves { - tmp.SetFromDecimal(entityPool.Reserves[i]) + err := tmp.SetFromDecimal(entityPool.Reserves[i]) + if err != nil { + return err + } if sim.Pool.Info.Reserves[i] == nil { sim.Pool.Info.Reserves[i] = new(big.Int) } From 9133d601f141bde8b7ade158c3328344232fb6c9 Mon Sep 17 00:00:00 2001 From: "le.cao" Date: Thu, 25 Jan 2024 10:55:03 +0700 Subject: [PATCH 3/4] add comment --- pkg/liquidity-source/uniswap-v2/pool_simulator.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/liquidity-source/uniswap-v2/pool_simulator.go b/pkg/liquidity-source/uniswap-v2/pool_simulator.go index 87e3a6eda..d32d00200 100644 --- a/pkg/liquidity-source/uniswap-v2/pool_simulator.go +++ b/pkg/liquidity-source/uniswap-v2/pool_simulator.go @@ -73,6 +73,10 @@ func NewPoolSimulatorV2(entityPool entity.Pool) (*PoolSimulator, error) { const NUM_TOKEN = 2 func InitPoolSimulator(entityPool entity.Pool, sim *PoolSimulator) error { + if len(entityPool.Tokens) != NUM_TOKEN || len(entityPool.Reserves) != NUM_TOKEN { + return errors.New("Invalid number of token") + } + var extra Extra if err := json.Unmarshal([]byte(entityPool.Extra), &extra); err != nil { return err @@ -85,10 +89,7 @@ func InitPoolSimulator(entityPool entity.Pool, sim *PoolSimulator) error { sim.Pool.Info.BlockNumber = entityPool.BlockNumber sim.gas = defaultGas - if len(entityPool.Tokens) != NUM_TOKEN || len(entityPool.Reserves) != NUM_TOKEN { - return errors.New("Invalid number of token") - } - + // try to re-use existing array if possible, if not then allocate new one if cap(sim.Pool.Info.Tokens) >= NUM_TOKEN { sim.Pool.Info.Tokens = sim.Pool.Info.Tokens[:NUM_TOKEN] } else { @@ -105,6 +106,9 @@ func InitPoolSimulator(entityPool entity.Pool, sim *PoolSimulator) error { } var tmp uint256.Int for i := range entityPool.Reserves { + // still not sure why, but uint256.SetFromDecimal doesn't use `strings.NewReader` like bigInt.SetString + // so it's cheaper to convert string to uint256 then to bigInt + // (in the far future if we can replace pool's reserves with uint256 then we can remove the last step) err := tmp.SetFromDecimal(entityPool.Reserves[i]) if err != nil { return err From 415bf97cd97f1e88b487a58428ba6a476d6acd24 Mon Sep 17 00:00:00 2001 From: "le.cao" Date: Fri, 26 Jan 2024 09:23:44 +0700 Subject: [PATCH 4/4] check sim nil --- pkg/liquidity-source/uniswap-v2/pool_simulator.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/liquidity-source/uniswap-v2/pool_simulator.go b/pkg/liquidity-source/uniswap-v2/pool_simulator.go index d32d00200..3b3142caf 100644 --- a/pkg/liquidity-source/uniswap-v2/pool_simulator.go +++ b/pkg/liquidity-source/uniswap-v2/pool_simulator.go @@ -76,6 +76,10 @@ func InitPoolSimulator(entityPool entity.Pool, sim *PoolSimulator) error { if len(entityPool.Tokens) != NUM_TOKEN || len(entityPool.Reserves) != NUM_TOKEN { return errors.New("Invalid number of token") } + // in case the caller mess thing up + if sim == nil { + return errors.New("Invalid simulator instance") + } var extra Extra if err := json.Unmarshal([]byte(entityPool.Extra), &extra); err != nil {