Skip to content

Commit

Permalink
Use github.com/holiman/uint256
Browse files Browse the repository at this point in the history
  • Loading branch information
tamirms committed Dec 1, 2021
1 parent 10c56f5 commit a0aa6cc
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 88 deletions.
111 changes: 27 additions & 84 deletions exp/orderbook/pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package orderbook

import (
"math"
"math/bits"

"lukechampine.com/uint128"

"github.com/holiman/uint256"
"github.com/stellar/go/support/errors"
"github.com/stellar/go/xdr"
)
Expand All @@ -22,16 +20,13 @@ import (
const (
tradeTypeDeposit = iota // deposit into pool, what's the payout?
tradeTypeExpectation = iota // expect payout, what to deposit?
maxBips = 10000
)

var (
errPoolOverflows = errors.New("Liquidity pool overflows from this exchange")
errBadPoolType = errors.New("Unsupported liquidity pool: must be ConstantProduct")
errBadTradeType = errors.New("Unknown pool exchange type requested")
errBadAmount = errors.New("Exchange amount must be positive")
one128 = uint128.Uint128{1, 0}
maxBips128 = uint128.From64(maxBips)
)

// makeTrade simulates execution of an exchange with a liquidity pool.
Expand Down Expand Up @@ -89,75 +84,39 @@ func makeTrade(
return result, nil
}

func mulWithOverflowCheck(u, v uint128.Uint128) (uint128.Uint128, bool) {
hi, lo := bits.Mul64(u.Lo, v.Lo)
p0, p1 := bits.Mul64(u.Hi, v.Lo)
p2, p3 := bits.Mul64(u.Lo, v.Hi)
hi, c0 := bits.Add64(hi, p1, 0)
hi, c1 := bits.Add64(hi, p3, c0)
overflew := (u.Hi != 0 && v.Hi != 0) || p0 != 0 || p2 != 0 || c1 != 0
return uint128.Uint128{lo, hi}, !overflew
}

func addWithOverflowCheck(u, v uint128.Uint128) (uint128.Uint128, bool) {
lo, carry := bits.Add64(u.Lo, v.Lo, 0)
hi, carry := bits.Add64(u.Hi, v.Hi, carry)
return uint128.Uint128{lo, hi}, carry == 0
}

// calculatePoolPayout calculates the amount of `reserveB` disbursed from the
// pool for a `received` amount of `reserveA` . From CAP-38:
//
// y = floor[(1 - F) Yx / (X + x - Fx)]
//
// It returns false if the calculation overflows.
func calculatePoolPayout(reserveA, reserveB, received xdr.Int64, feeBips xdr.Int32) (xdr.Int64, bool) {
X, Y := uint128.From64(uint64(reserveA)), uint128.From64(uint64(reserveB))
F, x := uint128.From64(uint64(feeBips)), uint128.From64(uint64(received))
X, Y := uint256.NewInt(uint64(reserveA)), uint256.NewInt(uint64(reserveB))
F, x := uint256.NewInt(uint64(feeBips)), uint256.NewInt(uint64(received))

// would this deposit overflow the reserve?
// is feeBips within range?
if received > math.MaxInt64-reserveA {
return 0, false
}

// We do all of the math in bips, so it's all upscaled by this value.
if feeBips > maxBips {
return 0, false
}
f := maxBips128.Sub(F) // upscaled 1 - F
maxBips := uint256.NewInt(10000)
f := new(uint256.Int).Sub(maxBips, F) // upscaled 1 - F

// right half: X + (1 - F)x
denom, ok := mulWithOverflowCheck(X, maxBips128)
if !ok {
return 0, false
}
xMulf, ok := mulWithOverflowCheck(x, f)
if !ok {
return 0, false
}
denom, ok = addWithOverflowCheck(denom, xMulf)
if !ok {
return 0, false
}
if denom.IsZero() { // avoid div-by-zero panic
denom := X.Mul(X, maxBips).Add(X, new(uint256.Int).Mul(x, f))
if denom.Cmp(uint256.NewInt(0)) == 0 { // avoid div-by-zero panic
return 0, false
}

// left half, a: (1 - F) Yx
numer, ok := mulWithOverflowCheck(Y, x)
if !ok {
return 0, false
}
numer, ok = mulWithOverflowCheck(numer, f)
if !ok {
return 0, false
}
numer := Y.Mul(Y, x).Mul(Y, f)

// divide & check overflow
result := numer.Div(denom)
result := numer.Div(numer, denom)

return xdr.Int64(result.Lo), result.Hi == 0 && result.Lo <= math.MaxInt64
val := xdr.Int64(result.Uint64())
return val, result.IsUint64() && val >= 0
}

// calculatePoolExpectation determines how much of `reserveA` you would need to
Expand All @@ -169,52 +128,36 @@ func calculatePoolPayout(reserveA, reserveB, received xdr.Int64, feeBips xdr.Int
func calculatePoolExpectation(
reserveA, reserveB, disbursed xdr.Int64, feeBips xdr.Int32,
) (xdr.Int64, bool) {
X, Y := uint128.From64(uint64(reserveA)), uint128.From64(uint64(reserveB))
F, y := uint128.From64(uint64(feeBips)), uint128.From64(uint64(disbursed))
X, Y := uint256.NewInt(uint64(reserveA)), uint256.NewInt(uint64(reserveB))
F, y := uint256.NewInt(uint64(feeBips)), uint256.NewInt(uint64(disbursed))

// We do all of the math in bips, so it's all upscaled by this value.
if feeBips > maxBips {
return 0, false
}
f := maxBips128.Sub(F) // upscaled 1 - F

// right half: (Y - y)(1 - F)
// sanity check: disbursing shouldn't underflow the reserve
if disbursed >= reserveB {
return 0, false
}
denom := Y.Sub(y)
var ok bool
denom, ok = mulWithOverflowCheck(denom, f)
if !ok {
return 0, false
}

if denom.IsZero() { // avoid div-by-zero panic
return 0, false
}
// We do all of the math in bips, so it's all upscaled by this value.
maxBips := uint256.NewInt(10000)
f := new(uint256.Int).Sub(maxBips, F) // upscaled 1 - F

// left half: Xy
numer, ok := mulWithOverflowCheck(X, y)
if !ok {
return 0, false
}
numer, ok = mulWithOverflowCheck(numer, maxBips128)
if !ok {
denom := Y.Sub(Y, y).Mul(Y, f) // right half: (Y - y)(1 - F)
if denom.Cmp(uint256.NewInt(0)) == 0 { // avoid div-by-zero panic
return 0, false
}

result, rem := numer.QuoRem(denom)
numer := X.Mul(X, y).Mul(X, maxBips) // left half: Xy

result, rem := new(uint256.Int), new(uint256.Int)
result.Div(numer, denom)
rem.Mod(numer, denom)

// hacky way to ceil(): if there's a remainder, add 1
if rem.Cmp64(0) > 0 {
result, ok = addWithOverflowCheck(result, one128)
if !ok {
return 0, false
}
if rem.Cmp(uint256.NewInt(0)) > 0 {
result.AddUint64(result, 1)
}

return xdr.Int64(result.Lo), result.Hi == 0 && result.Lo <= math.MaxInt64
val := xdr.Int64(result.Uint64())
return val, result.IsUint64() && val >= 0
}

// getOtherAsset returns the other asset in the liquidity pool. Note that
Expand Down
104 changes: 104 additions & 0 deletions exp/orderbook/pools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package orderbook

import (
"math"
"math/big"
"math/rand"
"testing"

"github.com/stellar/go/xdr"
Expand Down Expand Up @@ -218,3 +220,105 @@ func assertPoolExchange(t *testing.T,
assert.EqualValues(t, expectedDeposited, toPool, "wrong expectation")
}
}

func TestCalculatePoolExpectations(t *testing.T) {
for i := 0; i < 1000000; i++ {
reserveA := xdr.Int64(rand.Int63())
reserveB := xdr.Int64(rand.Int63())
disbursed := xdr.Int64(rand.Int63())

result, ok := calculatePoolExpectationBig(reserveA, reserveB, disbursed, 30)
result1, ok1 := calculatePoolExpectation(reserveA, reserveB, disbursed, 30)
if assert.Equal(t, ok, ok1) {
assert.Equal(t, result, result1)
}
}
}

func TestCalculatePoolPayout(t *testing.T) {
for i := 0; i < 1000000; i++ {
reserveA := xdr.Int64(rand.Int63())
reserveB := xdr.Int64(rand.Int63())
received := xdr.Int64(rand.Int63())

result, ok := calculatePoolPayoutBig(reserveA, reserveB, received, 30)
result1, ok1 := calculatePoolPayout(reserveA, reserveB, received, 30)
if assert.Equal(t, ok, ok1) {
assert.Equal(t, result, result1)
}
}
}

// calculatePoolPayout calculates the amount of `reserveB` disbursed from the
// pool for a `received` amount of `reserveA` . From CAP-38:
//
// y = floor[(1 - F) Yx / (X + x - Fx)]
//
// It returns false if the calculation overflows.
func calculatePoolPayoutBig(reserveA, reserveB, received xdr.Int64, feeBips xdr.Int32) (xdr.Int64, bool) {
X, Y := big.NewInt(int64(reserveA)), big.NewInt(int64(reserveB))
F, x := big.NewInt(int64(feeBips)), big.NewInt(int64(received))

// would this deposit overflow the reserve?
if received > math.MaxInt64-reserveA {
return 0, false
}

// We do all of the math in bips, so it's all upscaled by this value.
maxBips := big.NewInt(10000)
f := new(big.Int).Sub(maxBips, F) // upscaled 1 - F

// right half: X + (1 - F)x
denom := X.Mul(X, maxBips).Add(X, new(big.Int).Mul(x, f))
if denom.Cmp(big.NewInt(0)) == 0 { // avoid div-by-zero panic
return 0, false
}

// left half, a: (1 - F) Yx
numer := Y.Mul(Y, x).Mul(Y, f)

// divide & check overflow
result := numer.Div(numer, denom)

i := xdr.Int64(result.Int64())
return i, result.IsInt64() && i >= 0
}

// calculatePoolExpectation determines how much of `reserveA` you would need to
// put into a pool to get the `disbursed` amount of `reserveB`.
//
// x = ceil[Xy / ((Y - y)(1 - F))]
//
// It returns false if the calculation overflows.
func calculatePoolExpectationBig(
reserveA, reserveB, disbursed xdr.Int64, feeBips xdr.Int32,
) (xdr.Int64, bool) {
X, Y := big.NewInt(int64(reserveA)), big.NewInt(int64(reserveB))
F, y := big.NewInt(int64(feeBips)), big.NewInt(int64(disbursed))

// sanity check: disbursing shouldn't underflow the reserve
if disbursed >= reserveB {
return 0, false
}

// We do all of the math in bips, so it's all upscaled by this value.
maxBips := big.NewInt(10000)
f := new(big.Int).Sub(maxBips, F) // upscaled 1 - F

denom := Y.Sub(Y, y).Mul(Y, f) // right half: (Y - y)(1 - F)
if denom.Cmp(big.NewInt(0)) == 0 { // avoid div-by-zero panic
return 0, false
}

numer := X.Mul(X, y).Mul(X, maxBips) // left half: Xy

result, rem := new(big.Int), new(big.Int)
result.DivMod(numer, denom, rem)

// hacky way to ceil(): if there's a remainder, add 1
if rem.Cmp(big.NewInt(0)) > 0 {
result.Add(result, big.NewInt(1))
}

return xdr.Int64(result.Int64()), result.IsInt64()
}
2 changes: 1 addition & 1 deletion go.list
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ github.com/gorilla/schema v1.1.0
github.com/graph-gophers/graphql-go v0.0.0-20190225005345-3e8838d4614c
github.com/guregu/null v2.1.3-0.20151024101046-79c5bd36b615+incompatible
github.com/hashicorp/golang-lru v0.5.1
github.com/holiman/uint256 v1.2.0
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
github.com/hpcloud/tail v1.0.0
github.com/imkira/go-interpol v1.1.0
Expand Down Expand Up @@ -103,4 +104,3 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
gopkg.in/tylerb/graceful.v1 v1.2.13
gopkg.in/yaml.v2 v2.2.8
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
lukechampine.com/uint128 v1.1.1
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/gorilla/schema v1.1.0
github.com/graph-gophers/graphql-go v0.0.0-20190225005345-3e8838d4614c
github.com/guregu/null v2.1.3-0.20151024101046-79c5bd36b615+incompatible
github.com/holiman/uint256 v1.2.0
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
github.com/imkira/go-interpol v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
Expand Down Expand Up @@ -83,5 +84,4 @@ require (
gopkg.in/gorp.v1 v1.7.1 // indirect
gopkg.in/square/go-jose.v2 v2.4.1
gopkg.in/tylerb/graceful.v1 v1.2.13
lukechampine.com/uint128 v1.1.1
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ github.com/guregu/null v2.1.3-0.20151024101046-79c5bd36b615+incompatible/go.mod
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM=
github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
Expand Down Expand Up @@ -748,8 +750,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

0 comments on commit a0aa6cc

Please sign in to comment.