diff --git a/_deploy/r/gnoswap/common/access.gno b/_deploy/r/gnoswap/common/access.gno index c96bc2ef..0297c4a6 100644 --- a/_deploy/r/gnoswap/common/access.gno +++ b/_deploy/r/gnoswap/common/access.gno @@ -11,6 +11,7 @@ const ( ErrNoPermission = "caller(%s) has no permission" ) +// AssertCaller checks if the caller is the given address. func AssertCaller(caller, addr std.Address) error { if caller != addr { return ufmt.Errorf(ErrNoPermission, caller.String()) @@ -25,6 +26,7 @@ func SatisfyCond(cond bool) error { return nil } +// AdminOnly checks if the caller is the admin. func AdminOnly(caller std.Address) error { if caller != consts.ADMIN { return ufmt.Errorf(ErrNoPermission, caller.String()) @@ -32,6 +34,7 @@ func AdminOnly(caller std.Address) error { return nil } +// GovernanceOnly checks if the caller is the gov governance contract. func GovernanceOnly(caller std.Address) error { if caller != consts.GOV_GOVERNANCE_ADDR { return ufmt.Errorf(ErrNoPermission, caller.String()) @@ -39,6 +42,7 @@ func GovernanceOnly(caller std.Address) error { return nil } +// GovStakerOnly checks if the caller is the gov staker contract. func GovStakerOnly(caller std.Address) error { if caller != consts.GOV_STAKER_ADDR { return ufmt.Errorf(ErrNoPermission, caller.String()) @@ -46,6 +50,7 @@ func GovStakerOnly(caller std.Address) error { return nil } +// RouterOnly checks if the caller is the router contract. func RouterOnly(caller std.Address) error { if caller != consts.ROUTER_ADDR { return ufmt.Errorf(ErrNoPermission, caller.String()) @@ -53,6 +58,7 @@ func RouterOnly(caller std.Address) error { return nil } +// PositionOnly checks if the caller is the position contract. func PositionOnly(caller std.Address) error { if caller != consts.POSITION_ADDR { return ufmt.Errorf(ErrNoPermission, caller.String()) @@ -60,6 +66,7 @@ func PositionOnly(caller std.Address) error { return nil } +// StakerOnly checks if the caller is the staker contract. func StakerOnly(caller std.Address) error { if caller != consts.STAKER_ADDR { return ufmt.Errorf(ErrNoPermission, caller.String()) @@ -67,6 +74,7 @@ func StakerOnly(caller std.Address) error { return nil } +// LaunchpadOnly checks if the caller is the launchpad contract. func LaunchpadOnly(caller std.Address) error { if caller != consts.LAUNCHPAD_ADDR { return ufmt.Errorf(ErrNoPermission, caller.String()) @@ -74,6 +82,7 @@ func LaunchpadOnly(caller std.Address) error { return nil } +// EmissionOnly checks if the caller is the emission contract. func EmissionOnly(caller std.Address) error { if caller != consts.EMISSION_ADDR { return ufmt.Errorf(ErrNoPermission, caller.String()) @@ -90,8 +99,7 @@ func TokenRegisterOnly(caller std.Address) error { return nil } -// realm assertion - +// UserOnly checks if the caller is a user. func UserOnly(prev std.Realm) error { if !prev.IsUser() { return ufmt.Errorf("caller(%s) is not a user", prev.PkgPath()) diff --git a/_deploy/r/gnoswap/common/address_and_username.gno b/_deploy/r/gnoswap/common/address_and_username.gno index 708c55e2..908f8171 100644 --- a/_deploy/r/gnoswap/common/address_and_username.gno +++ b/_deploy/r/gnoswap/common/address_and_username.gno @@ -24,6 +24,6 @@ func UserToAddr(user pusers.AddressOrName) std.Address { // It panics with a detailed error message if the address is invalid. func assertValidAddr(addr std.Address) { if !addr.IsValid() { - panic(addDetailToError(errInvalidAddr, addr.String())) + panic(newErrorWithDetail(errInvalidAddr, addr.String())) } } diff --git a/_deploy/r/gnoswap/common/errors.gno b/_deploy/r/gnoswap/common/errors.gno index 168d8b85..f9612447 100644 --- a/_deploy/r/gnoswap/common/errors.gno +++ b/_deploy/r/gnoswap/common/errors.gno @@ -14,9 +14,19 @@ var ( errInvalidAddr = errors.New("[GNOSWAP-COMMON-005] invalid address") errOverflow = errors.New("[GNOSWAP-COMMON-006] overflow") errInvalidTokenId = errors.New("[GNOSWAP-COMMON-007] invalid tokenId") + errInvalidInput = errors.New("[GNOSWAP-COMMON-008] invalid input data") + errOverFlow = errors.New("[GNOSWAP-COMMON-009] overflow") + errIdenticalTicks = errors.New("[GNOSWAP-COMMON-010] identical ticks") ) -func addDetailToError(err error, detail string) string { - finalErr := ufmt.Errorf("%s || %s", err.Error(), detail) - return finalErr.Error() +// newErrorWithDetail appends additional context or details to an existing error message. +// +// Parameters: +// - err: The original error (error). +// - detail: Additional context or detail to append to the error message (string). +// +// Returns: +// - string: The combined error message in the format " || ". +func newErrorWithDetail(err error, detail string) string { + return ufmt.Errorf("%s || %s", err.Error(), detail).Error() } diff --git a/_deploy/r/gnoswap/common/grc20reg_helper.gno b/_deploy/r/gnoswap/common/grc20reg_helper.gno index 992a0270..eeba0328 100644 --- a/_deploy/r/gnoswap/common/grc20reg_helper.gno +++ b/_deploy/r/gnoswap/common/grc20reg_helper.gno @@ -51,9 +51,9 @@ func IsRegistered(path string) error { // if token is not registered, it will panic func MustRegistered(path string) { if err := IsRegistered(path); err != nil { - panic(addDetailToError( + panic(newErrorWithDetail( errNotRegistered, - ufmt.Sprintf("token(%s)", path), + ufmt.Sprintf("token(%s) is not registered", path), )) } } diff --git a/_deploy/r/gnoswap/common/grc721_token_id.gno b/_deploy/r/gnoswap/common/grc721_token_id.gno index 2cc4c562..033fb499 100644 --- a/_deploy/r/gnoswap/common/grc721_token_id.gno +++ b/_deploy/r/gnoswap/common/grc721_token_id.gno @@ -16,7 +16,7 @@ import ( // output: grc721.TokenID func TokenIdFrom(tokenId interface{}) grc721.TokenID { if tokenId == nil { - panic(addDetailToError( + panic(newErrorWithDetail( errInvalidTokenId, "can not be nil", )) @@ -33,7 +33,7 @@ func TokenIdFrom(tokenId interface{}) grc721.TokenID { return tokenId.(grc721.TokenID) default: estimatedType := ufmt.Sprintf("%T", tokenId) - panic(addDetailToError( + panic(newErrorWithDetail( errInvalidTokenId, ufmt.Sprintf("unsupported tokenId type: %s", estimatedType), )) diff --git a/_deploy/r/gnoswap/common/halt.gno b/_deploy/r/gnoswap/common/halt.gno index 9856a75c..854c9c0e 100644 --- a/_deploy/r/gnoswap/common/halt.gno +++ b/_deploy/r/gnoswap/common/halt.gno @@ -4,33 +4,49 @@ import ( "std" "gno.land/p/demo/ufmt" - "gno.land/r/gnoswap/v1/consts" ) -var ( - halted bool = false -) +// halted is a global flag that indicates whether the system is currently halted. +// When true, most operations are disabled to prevent further actions. +// Default value is false, meaning the system is active by default. +var halted bool = false +// GetHalt returns the current halted status of the system. +// +// Returns: +// - bool: true if the system is halted, false otherwise. func GetHalt() bool { return halted } +// IsHalted checks if the system is currently halted. +// If the system is halted, the function panics with an errHalted error. +// +// Panics: +// - If the halted flag is true, indicating that the system is inactive. func IsHalted() { if halted { - panic(addDetailToError( + panic(newErrorWithDetail( errHalted, - "gnoswap halted", + "GnoSwap is halted", )) } } -// SetHaltByAdmin sets the halt status. +// SetHaltByAdmin allows an admin to set the halt status of the system. +// Only an admin can execute this function. If a non-admin attempts to call this function, +// the function panics with an errNoPermission error. +// +// Parameters: +// - halt (bool): The new halt status to set (true to halt, false to unhalt). +// +// Panics: +// - If the caller is not an admin, the function will panic with an errNoPermission error. func SetHaltByAdmin(halt bool) { - caller := std.PrevRealm().Addr() - err := AdminOnly(caller) - if err != nil { - panic(addDetailToError( + caller := getPrevAddr() + if err := AdminOnly(caller); err != nil { + panic(newErrorWithDetail( errNoPermission, ufmt.Sprintf( "only admin(%s) can set halt, called from %s", @@ -39,34 +55,22 @@ func SetHaltByAdmin(halt bool) { ), )) } - setHalt(halt) - - prevAddr, prevRealm := getPrev() - if halt { - std.Emit( - "SetHaltByAdmin", - "prevAddr", prevAddr, - "prevRealm", prevRealm, - "halt", ufmt.Sprintf("%t", halt), - ) - } else { - std.Emit( - "UnsetHaltByAdmin", - "prevAddr", prevAddr, - "prevRealm", prevRealm, - "halt", ufmt.Sprintf("%t", halt), - ) - } } -// SetHalt sets the halt status. -// Only governance contract can execute this function via proposal +// SetHalt allows the governance contract to set the halt status of the system. +// Only the governance contract can execute this function through a proposal process. +// If the caller is not the governance contract, the function panics with an errNoPermission error. +// +// Parameters: +// - halt (bool): The new halt status to set (true to halt, false to unhalt). +// +// Panics: +// - If the caller is not the governance contract, the function will panic with an errNoPermission error. func SetHalt(halt bool) { - caller := std.PrevRealm().Addr() - err := GovernanceOnly(caller) - if err != nil { - panic(addDetailToError( + caller := getPrevAddr() + if err := GovernanceOnly(caller); err != nil { + panic(newErrorWithDetail( errNoPermission, ufmt.Sprintf( "only governance(%s) can set halt, called from %s", @@ -75,27 +79,22 @@ func SetHalt(halt bool) { ), )) } - setHalt(halt) - - prevAddr, prevRealm := getPrev() - if halt { - std.Emit( - "SetHalt", - "prevAddr", prevAddr, - "prevRealm", prevRealm, - "halt", ufmt.Sprintf("%t", halt), - ) - } else { - std.Emit( - "UnsetHalt", - "prevAddr", prevAddr, - "prevRealm", prevRealm, - "halt", ufmt.Sprintf("%t", halt), - ) - } } +// setHalt updates the halted flag to the specified value. +// This is an internal function that should only be called by SetHalt or SetHaltByAdmin. +// +// Parameters: +// - halt (bool): The new halt status to set. func setHalt(halt bool) { halted = halt + + prevAddr, prevRealm := getPrevAsString() + std.Emit( + "setHalt", + "prevAddr", prevAddr, + "prevRealm", prevRealm, + "halt", ufmt.Sprintf("%t", halt), + ) } diff --git a/_deploy/r/gnoswap/common/limit_caller.gno b/_deploy/r/gnoswap/common/limit_caller.gno index d376b760..4d4d1df5 100644 --- a/_deploy/r/gnoswap/common/limit_caller.gno +++ b/_deploy/r/gnoswap/common/limit_caller.gno @@ -4,25 +4,49 @@ import ( "std" "gno.land/p/demo/ufmt" - - "gno.land/r/gnoswap/v1/consts" ) -var ( - limitCaller bool = true -) +// limitCaller is a global boolean flag that controls whether function calls are restricted. +// Default value is true, meaning call restrictions are enabled by default. +var limitCaller bool = true +// GetLimitCaller returns the current state of the limitCaller flag. +// If true, call restrictions are active; if false, call restrictions are disabled. +// +// Returns: +// - bool: Current state of the limitCaller (true if active, false if inactive). func GetLimitCaller() bool { return limitCaller } +// SetLimitCaller updates the limitCaller flag to either enable or disable call restrictions. +// This function can only be called by an admin. If a non-admin attempts to call this function, +// the function will panic. +// +// Parameters: +// - v (bool): The new state for the limitCaller flag (true to enable, false to disable). +// +// Panics: +// - If the caller is not an admin, the function panics with an errNoPermission error. func SetLimitCaller(v bool) { - caller := std.PrevRealm().Addr() - if caller != consts.ADMIN { - panic(addDetailToError( + caller := getPrevAddr() + if err := AdminOnly(caller); err != nil { + panic(newErrorWithDetail( errNoPermission, - ufmt.Sprintf("limit_caller.gno__SetLimitCaller() || only admin(%s) can set limit caller, called from %s", consts.ADMIN, caller), - )) + ufmt.Sprintf( + "only Admin can set halt, called from %s", + caller, + )), + ) } + limitCaller = v + + prevAddr, prevPkgPath := getPrevAsString() + std.Emit( + "SetLimitCaller", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "halt", ufmt.Sprintf("%t", v), + ) } diff --git a/_deploy/r/gnoswap/common/tests/limit_caller_test.gno b/_deploy/r/gnoswap/common/limit_caller_test.gno similarity index 62% rename from _deploy/r/gnoswap/common/tests/limit_caller_test.gno rename to _deploy/r/gnoswap/common/limit_caller_test.gno index 7b98961a..d8007140 100644 --- a/_deploy/r/gnoswap/common/tests/limit_caller_test.gno +++ b/_deploy/r/gnoswap/common/limit_caller_test.gno @@ -5,12 +5,9 @@ import ( "testing" "gno.land/p/demo/uassert" - "gno.land/r/gnoswap/v1/consts" ) -var adminRealm = std.NewUserRealm(consts.ADMIN) - func TestSetLimitCaller(t *testing.T) { t.Run("initial check", func(t *testing.T) { uassert.True(t, GetLimitCaller()) @@ -18,13 +15,13 @@ func TestSetLimitCaller(t *testing.T) { t.Run("with non-admin privilege, panics", func(t *testing.T) { uassert.PanicsWithMessage(t, - `[GNOSWAP-COMMON-001] caller has no permission || limit_caller.gno__SetLimitCaller() || only admin(g17290cwvmrapvp869xfnhhawa8sm9edpufzat7d) can set limit caller, called from g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm`, + `[GNOSWAP-COMMON-001] caller has no permission || only Admin can set halt, called from g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm`, func() { SetLimitCaller(false) }, ) }) t.Run("with admin privilege, success", func(t *testing.T) { - std.TestSetRealm(adminRealm) + std.TestSetRealm(std.NewUserRealm(consts.ADMIN)) SetLimitCaller(false) uassert.False(t, GetLimitCaller()) }) diff --git a/_deploy/r/gnoswap/common/liquidity_amounts.gno b/_deploy/r/gnoswap/common/liquidity_amounts.gno index f312e656..09be46b6 100644 --- a/_deploy/r/gnoswap/common/liquidity_amounts.gno +++ b/_deploy/r/gnoswap/common/liquidity_amounts.gno @@ -1,15 +1,12 @@ package common import ( - i256 "gno.land/p/gnoswap/int256" + "gno.land/p/demo/ufmt" u256 "gno.land/p/gnoswap/uint256" - - plp "gno.land/p/gnoswap/pool" - "gno.land/r/gnoswap/v1/consts" ) -// toAscendingOrder checkes if the first value is greater than +// toAscendingOrder checks if the first value is greater than // the second then swaps two values. func toAscendingOrder(a, b *u256.Uint) (*u256.Uint, *u256.Uint) { if a.Gt(b) { @@ -19,119 +16,299 @@ func toAscendingOrder(a, b *u256.Uint) (*u256.Uint, *u256.Uint) { return a, b } -// computeLiquidityForAmount0 calculates liquidity for a given amount of token 0. +// toUint128 ensures a *u256.Uint value fits within the uint128 range. +// +// This function validates that the given `value` is properly initialized (not nil) and checks whether +// it exceeds the maximum value of uint128. If the value exceeds the uint128 range, +// it applies a masking operation to truncate the value to fit within the uint128 limit. +// +// Parameters: +// - value: *u256.Uint, the value to be checked and possibly truncated. +// +// Returns: +// - *u256.Uint: A value guaranteed to fit within the uint128 range. +// +// Notes: +// - The function first checks if the value is not nil to avoid potential runtime errors. +// - The mask ensures that only the lower 128 bits of the value are retained. +// - If the input value is already within the uint128 range, it remains unchanged. +// - MAX_UINT128 is a constant representing `2^128 - 1`. +func toUint128(value *u256.Uint) *u256.Uint { + assertOnlyNotNil(value) + if value.Gt(u256.MustFromDecimal(consts.MAX_UINT128)) { + mask := new(u256.Uint).Lsh(u256.One(), consts.Q128_RESOLUTION) + mask = new(u256.Uint).Sub(mask, u256.One()) + value = value.And(value, mask) + } + return value +} + +// safeConvertToUint128 safely ensures a *u256.Uint value fits within the uint128 range. +// +// This function verifies that the provided unsigned 256-bit integer does not exceed the maximum value for uint128 (`2^128 - 1`). +// If the value is within the uint128 range, it is returned as is; otherwise, the function triggers a panic. +// +// Parameters: +// - value (*u256.Uint): The unsigned 256-bit integer to be checked. +// +// Returns: +// - *u256.Uint: The same value if it is within the uint128 range. +// +// Panics: +// - If the value exceeds the maximum uint128 value (`2^128 - 1`), the function will panic with a descriptive error +// indicating the overflow and the original value. +// +// Notes: +// - The constant `MAX_UINT128` is defined as `340282366920938463463374607431768211455` (the largest uint128 value). +// - No actual conversion occurs since the function works directly with *u256.Uint types. +// +// Example: +// validUint128 := safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211455")) // Valid +// safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211456")) // Panics due to overflow +func safeConvertToUint128(value *u256.Uint) *u256.Uint { + if value.Gt(u256.MustFromDecimal(consts.MAX_UINT128)) { + panic(ufmt.Sprintf( + "%v: amount(%s) overflows uint128 range", + errOverFlow, value.ToString())) + } + return value +} + +// computeLiquidityForAmount0 calculates the liquidity for a given amount of token0. +// +// This function computes the maximum possible liquidity that can be provided for `token0` +// based on the provided price boundaries (sqrtRatioAX96 and sqrtRatioBX96) in Q64.96 format. +// +// Parameters: +// - sqrtRatioAX96: *u256.Uint - The square root price at the lower tick boundary (Q64.96). +// - sqrtRatioBX96: *u256.Uint - The square root price at the upper tick boundary (Q64.96). +// - amount0: *u256.Uint - The amount of token0 to be converted to liquidity. +// +// Returns: +// - *u256.Uint: The calculated liquidity, represented as an unsigned 128-bit integer (uint128). +// +// Panics: +// - If the resulting liquidity exceeds the uint128 range, `safeConvertToUint128` will trigger a panic. func computeLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0 *u256.Uint) *u256.Uint { sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) - intermediate := u256.MulDiv(sqrtRatioAX96, sqrtRatioBX96, u256.MustFromDecimal(consts.Q96)) diff := new(u256.Uint).Sub(sqrtRatioBX96, sqrtRatioAX96) - + if diff.IsZero() { + panic(newErrorWithDetail( + errIdenticalTicks, + ufmt.Sprintf("sqrtRatioAX96 (%s) and sqrtRatioBX96 (%s) are identical", sqrtRatioAX96.ToString(), sqrtRatioBX96.ToString()), + )) + } res := u256.MulDiv(amount0, intermediate, diff) - return res + return safeConvertToUint128(res) } -// computeLiquidityForAmount1 calculates liquidity for a given amount of token 1. +// computeLiquidityForAmount1 calculates liquidity based on the provided token1 amount and price range. +// +// This function computes the liquidity for a given amount of token1 by using the difference +// between the upper and lower square root price ratios. The calculation uses Q96 fixed-point +// arithmetic to maintain precision. +// +// Parameters: +// - sqrtRatioAX96: *u256.Uint - The square root ratio of price at the lower tick, represented in Q96 format. +// - sqrtRatioBX96: *u256.Uint - The square root ratio of price at the upper tick, represented in Q96 format. +// - amount1: *u256.Uint - The amount of token1 to calculate liquidity for. +// +// Returns: +// - *u256.Uint: The calculated liquidity based on the provided amount of token1 and price range. +// +// Notes: +// - The result is not directly limited to uint128, as liquidity values can exceed uint128 bounds. +// - If `sqrtRatioAX96 == sqrtRatioBX96`, the function will panic due to division by zero. +// - Q96 is a constant representing `2^96`, ensuring that precision is maintained during division. +// +// Panics: +// - If the resulting liquidity exceeds the uint128 range, `safeConvertToUint128` will trigger a panic. func computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1 *u256.Uint) *u256.Uint { sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) diff := new(u256.Uint).Sub(sqrtRatioBX96, sqrtRatioAX96) - + if diff.IsZero() { + panic(newErrorWithDetail( + errIdenticalTicks, + ufmt.Sprintf("sqrtRatioAX96 (%s) and sqrtRatioBX96 (%s) are identical", sqrtRatioAX96.ToString(), sqrtRatioBX96.ToString()), + )) + } res := u256.MulDiv(amount1, u256.MustFromDecimal(consts.Q96), diff) - return res + return safeConvertToUint128(res) } -// GetLiquidityForAmounts calculates the liquidity for given amounts od token 0 and token 1. +// GetLiquidityForAmounts calculates the maximum liquidity given the current price (sqrtRatioX96), +// upper and lower price bounds (sqrtRatioAX96 and sqrtRatioBX96), and token amounts (amount0, amount1). +// +// This function evaluates how much liquidity can be obtained for specified amounts of token0 and token1 +// within the provided price range. It returns the lesser liquidity based on available token0 or token1 +// to ensure the pool remains balanced. +// +// Parameters: +// - sqrtRatioX96: The current price as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - amount0: The amount of token0 available to provide liquidity (*u256.Uint). +// - amount1: The amount of token1 available to provide liquidity (*u256.Uint). +// +// Returns: +// - *u256.Uint: The maximum possible liquidity that can be minted. +// +// Notes: +// - The `Clone` method is used to prevent modification of the original values during computation. +// - The function ensures that liquidity calculations handle edge cases when the current price +// is outside the specified range by returning liquidity based on the dominant token. func GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1 *u256.Uint) *u256.Uint { - sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) + if amount0.IsZero() || amount1.IsZero() { + return u256.Zero() + } + + sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone()) var liquidity *u256.Uint if sqrtRatioX96.Lte(sqrtRatioAX96) { - liquidity = computeLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0) + liquidity = computeLiquidityForAmount0(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone(), amount0.Clone()) } else if sqrtRatioX96.Lt(sqrtRatioBX96) { - liquidity0 := computeLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0) - liquidity1 := computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1) + liquidity0 := computeLiquidityForAmount0(sqrtRatioX96.Clone(), sqrtRatioBX96.Clone(), amount0.Clone()) + liquidity1 := computeLiquidityForAmount1(sqrtRatioAX96.Clone(), sqrtRatioX96.Clone(), amount1.Clone()) if liquidity0.Lt(liquidity1) { liquidity = liquidity0 } else { liquidity = liquidity1 } - } else { - liquidity = computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1) + liquidity = computeLiquidityForAmount1(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone(), amount1.Clone()) } - return liquidity } -// computeAmount0ForLiquidity calculates the amount of token0 for a given liquidity. +// computeAmount0ForLiquidity calculates the required amount of token0 for a given liquidity level +// within a specified price range (represented by sqrt ratios). +// +// This function determines the amount of token0 needed to provide a specified amount of liquidity +// within a price range defined by sqrtRatioAX96 (lower bound) and sqrtRatioBX96 (upper bound). +// +// Parameters: +// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - liquidity: The liquidity to be provided (*u256.Uint). +// +// Returns: +// - *u256.Uint: The amount of token0 required to achieve the specified liquidity level. +// +// Notes: +// - This function assumes the price bounds are expressed in Q64.96 fixed-point format. +// - The function returns 0 if the liquidity is 0 or the price bounds are invalid. +// - Handles edge cases where sqrtRatioAX96 equals sqrtRatioBX96 by returning 0 (to prevent division by zero). func computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) *u256.Uint { sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) + if sqrtRatioAX96.IsZero() || sqrtRatioBX96.IsZero() || liquidity.IsZero() || sqrtRatioAX96.Eq(sqrtRatioBX96) { + return u256.Zero() + } - val1 := new(u256.Uint).Lsh(liquidity, 96) + val1 := new(u256.Uint).Lsh(liquidity, consts.Q96_RESOLUTION) val2 := new(u256.Uint).Sub(sqrtRatioBX96, sqrtRatioAX96) res := u256.MulDiv(val1, val2, sqrtRatioBX96) - - res = res.Div(res, sqrtRatioAX96) + res = new(u256.Uint).Div(res, sqrtRatioAX96) return res } -// computeAmount1ForLiquidity calculates the amount of token1 for a given liquidity. +// computeAmount1ForLiquidity calculates the required amount of token1 for a given liquidity level +// within a specified price range (represented by sqrt ratios). +// +// This function determines the amount of token1 needed to provide liquidity between the +// lower (sqrtRatioAX96) and upper (sqrtRatioBX96) price bounds. The calculation is performed +// in Q64.96 fixed-point format, which is standard for many liquidity calculations. +// +// Parameters: +// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - liquidity: The liquidity amount to be used in the calculation (*u256.Uint). +// +// Returns: +// - *u256.Uint: The amount of token1 required to achieve the specified liquidity level. +// +// Notes: +// - This function handles edge cases where the liquidity is zero or when sqrtRatioAX96 equals sqrtRatioBX96 +// to prevent division by zero. +// - The calculation assumes sqrtRatioAX96 is always less than or equal to sqrtRatioBX96 after the initial +// ascending order sorting. func computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) *u256.Uint { sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) + if liquidity.IsZero() || sqrtRatioAX96.Eq(sqrtRatioBX96) { + return u256.Zero() + } - val2 := new(u256.Uint).Sub(sqrtRatioBX96, sqrtRatioAX96) + diff := new(u256.Uint).Sub(sqrtRatioBX96, sqrtRatioAX96) + res := u256.MulDiv(liquidity, diff, u256.MustFromDecimal(consts.Q96)) - res := u256.MulDiv(liquidity, val2, u256.MustFromDecimal(consts.Q96)) return res } -// GetAmountsForLiquidity calculates the amounts of token0 and token1 for a given liquidity. -func GetAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, liquidity *i256.Int) (string, string) { - var amount0, amount1 *i256.Int - - if !(liquidity.IsZero()) { - if sqrtRatioX96.Lt(sqrtRatioAX96) { // currentTick < tickLower - _amount0Str := plp.SqrtPriceMathGetAmount0DeltaStr( - sqrtRatioAX96, - sqrtRatioBX96, - liquidity, - ) - amount0 = i256.MustFromDecimal(_amount0Str) - - } else if sqrtRatioX96.Lt(sqrtRatioBX96) { // currentTick < tickUpper - _amount0Str := plp.SqrtPriceMathGetAmount0DeltaStr( - sqrtRatioX96, - sqrtRatioBX96, - liquidity, - ) - amount0 = i256.MustFromDecimal(_amount0Str) - - _amount1Str := plp.SqrtPriceMathGetAmount1DeltaStr( - sqrtRatioAX96, - sqrtRatioX96, - liquidity, - ) - amount1 = i256.MustFromDecimal(_amount1Str) +// GetAmountsForLiquidity calculates the amounts of token0 and token1 required +// to provide a specified liquidity within a price range. +// +// This function determines the quantities of token0 and token1 necessary to achieve +// a given liquidity level, depending on the current price (sqrtRatioX96) and the +// bounds of the price range (sqrtRatioAX96 and sqrtRatioBX96). The function returns +// the calculated amounts of token0 and token1 as strings. +// +// If the current price is below the lower bound of the price range, only token0 is required. +// If the current price is above the upper bound, only token1 is required. When the +// price is within the range, both token0 and token1 are calculated. +// +// Parameters: +// - sqrtRatioX96: The current price represented as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - liquidity: The amount of liquidity to be provided (*u256.Uint). +// +// Returns: +// - string: The calculated amount of token0 required to achieve the specified liquidity. +// - string: The calculated amount of token1 required to achieve the specified liquidity. +// +// Notes: +// - If liquidity is zero, the function returns "0" for both token0 and token1. +// - The function guarantees that sqrtRatioAX96 is always the lower bound and +// sqrtRatioBX96 is the upper bound by calling toAscendingOrder(). +// - Edge cases where the current price is exactly on the bounds are handled without division by zero. +// +// Example: +// ``` +// amount0, amount1 := GetAmountsForLiquidity( +// +// u256.MustFromDecimal("79228162514264337593543950336"), // sqrtRatioX96 (1.0 in Q64.96) +// u256.MustFromDecimal("39614081257132168796771975168"), // sqrtRatioAX96 (0.5 in Q64.96) +// u256.MustFromDecimal("158456325028528675187087900672"), // sqrtRatioBX96 (2.0 in Q64.96) +// u256.MustFromDecimal("1000000"), // Liquidity +// +// ) +// fmt.Println("Token0:", amount0, "Token1:", amount1) +// // Example output: Token0: 500000, Token1: 250000 +// ``` +func GetAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) (string, string) { + if liquidity.IsZero() { + return "0", "0" + } - } else { - _amount1Str := plp.SqrtPriceMathGetAmount1DeltaStr( - sqrtRatioAX96, - sqrtRatioBX96, - liquidity, - ) - amount1 = i256.MustFromDecimal(_amount1Str) - } + sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) - } + amount0 := u256.Zero() + amount1 := u256.Zero() - // if position is out of range, one of amount0 or amount1 can be nil - // > handle as 0 - amount0 = amount0.NilToZero() - amount1 = amount1.NilToZero() + if sqrtRatioX96.Lte(sqrtRatioAX96) { + amount0 = computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) + } else if sqrtRatioX96.Lt(sqrtRatioBX96) { + amount0 = computeAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity) + amount1 = computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity) + } else { + amount1 = computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) + } return amount0.ToString(), amount1.ToString() } diff --git a/_deploy/r/gnoswap/common/liquidity_amounts_test.gno b/_deploy/r/gnoswap/common/liquidity_amounts_test.gno new file mode 100644 index 00000000..d471db2d --- /dev/null +++ b/_deploy/r/gnoswap/common/liquidity_amounts_test.gno @@ -0,0 +1,506 @@ +package common + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/gnoswap/v1/consts" +) + +func TestToAscendingOrder(t *testing.T) { + tests := []struct { + name string + a *u256.Uint + b *u256.Uint + expectedA string + expectedB string + }{ + { + name: "Ascending order - a < b", + a: u256.MustFromDecimal("10"), + b: u256.MustFromDecimal("20"), + expectedA: "10", + expectedB: "20", + }, + { + name: "Descending order - a > b", + a: u256.MustFromDecimal("50"), + b: u256.MustFromDecimal("30"), + expectedA: "30", + expectedB: "50", + }, + { + name: "Equal values - a == b", + a: u256.MustFromDecimal("100"), + b: u256.MustFromDecimal("100"), + expectedA: "100", + expectedB: "100", + }, + { + name: "Large numbers", + a: u256.MustFromDecimal("340282366920938463463374607431768211455"), // 2^128 - 1 + b: u256.MustFromDecimal("170141183460469231731687303715884105727"), // 2^127 - 1 + expectedA: "170141183460469231731687303715884105727", + expectedB: "340282366920938463463374607431768211455", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + min, max := toAscendingOrder(tt.a, tt.b) + + if min.ToString() != tt.expectedA { + t.Errorf("Expected min to be %s, got %s", tt.expectedA, min.ToString()) + } + + if max.ToString() != tt.expectedB { + t.Errorf("Expected max to be %s, got %s", tt.expectedB, max.ToString()) + } + }) + } +} + +func TestSafeConvertToUint128(t *testing.T) { + tests := []struct { + name string + input *u256.Uint + expected *u256.Uint + shouldPanic bool + }{ + { + name: "Valid uint128 value", + input: u256.MustFromDecimal("340282366920938463463374607431768211455"), // MAX_UINT128 + expected: u256.MustFromDecimal("340282366920938463463374607431768211455"), + shouldPanic: false, + }, + { + name: "Value exceeding uint128 range", + input: u256.MustFromDecimal("340282366920938463463374607431768211456"), // MAX_UINT128 + 1 + shouldPanic: true, + }, + { + name: "Zero value", + input: u256.Zero(), + expected: u256.Zero(), + shouldPanic: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic but got none") + } + }() + safeConvertToUint128(tt.input) + } else { + got := safeConvertToUint128(tt.input) + if !got.Eq(tt.expected) { + t.Errorf("Expected %s, got %s", tt.expected.ToString(), got.ToString()) + } + } + }) + } +} + +func TestComputeLiquidityForAmount0(t *testing.T) { + testCases := []struct { + name string + sqrtRatioA string + sqrtRatioB string + amount0 string + expected string + expectPanic bool + }{ + { + name: "Basic liquidity calculation", + sqrtRatioA: "79228162514264337593543950336", // sqrt(1) << 96 + sqrtRatioB: "158456325028528675187087900672", // sqrt(4) << 96 + amount0: "1000000", + expected: "2000000", // Expected liquidity + }, + { + name: "No liquidity (zero amount)", + sqrtRatioA: "79228162514264337593543950336", + sqrtRatioB: "158456325028528675187087900672", + amount0: "0", + expected: "0", + }, + { + name: "Liquidity overflow (exceeds uint128)", + sqrtRatioA: "158456325028528675187087900672", + sqrtRatioB: "316912650057057350374175801344", + amount0: "340282366920938463463374607431768211456", // Exceeds uint128 + expectPanic: true, + }, + { + name: "Zero liquidity with equal ratios", + sqrtRatioA: "79228162514264337593543950336", + sqrtRatioB: "79228162514264337593543950336", + amount0: "1000000", + expected: "0", + expectPanic: true, + }, + { + name: "Panic with identical ticks", + sqrtRatioA: "79228162514264337593543950336", + sqrtRatioB: "79228162514264337593543950336", + amount0: "1000000", + expectPanic: true, + }, + { + name: "Large liquidity calculation", + sqrtRatioA: "79228162514264337593543950336", + sqrtRatioB: "158456325028528675187087900672", + amount0: "1000000000", + expected: "2000000000", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tc.expectPanic { + t.Errorf("Unexpected panic for test case: %s", tc.name) + } + } + }() + + sqrtRatioA := u256.MustFromDecimal(tc.sqrtRatioA) + sqrtRatioB := u256.MustFromDecimal(tc.sqrtRatioB) + amount0 := u256.MustFromDecimal(tc.amount0) + + result := computeLiquidityForAmount0(sqrtRatioA, sqrtRatioB, amount0) + if !tc.expectPanic { + if result.ToString() != tc.expected { + t.Errorf("Expected %s but got %s", tc.expected, result.ToString()) + } + } + }) + } +} + +func TestComputeLiquidityForAmount1(t *testing.T) { + q96 := u256.MustFromDecimal(consts.Q96) + amount1 := u256.MustFromDecimal("1000000") + + t.Run("Basic liquidity calculation", func(t *testing.T) { + sqrtRatioAX96 := q96 // 2^96 (1 in Q96) + sqrtRatioBX96 := new(u256.Uint).Mul(q96, u256.MustFromDecimal("4")) // 4^96 (4 in Q96) + + expected := u256.MustFromDecimal("333333") // Expected liquidity + result := computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1) + + if !result.Eq(expected) { + t.Errorf("Expected %s but got %s", expected.ToString(), result.ToString()) + } + }) + + t.Run("Zero liquidity with equal ratios", func(t *testing.T) { + sqrtRatioAX96 := q96 // 2^96 (1 in Q96) + sqrtRatioBX96 := q96 // Same as lower tick + + uassert.PanicsWithMessage(t, + "[GNOSWAP-COMMON-010] identical ticks || sqrtRatioAX96 (79228162514264337593543950336) and sqrtRatioBX96 (79228162514264337593543950336) are identical", + func() { + _ = computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1) + }) + }) + + t.Run("Large liquidity calculation", func(t *testing.T) { + sqrtRatioAX96 := q96 // 1x + sqrtRatioBX96 := new(u256.Uint).Mul(q96, u256.NewUint(16)) // 16x + largeAmount := u256.MustFromDecimal("1000000000") + + expected := u256.MustFromDecimal("66666666") // 1B / 16 = 62.5M + result := computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, largeAmount) + + if !result.Eq(expected) { + t.Errorf("Expected %s but got %s", expected.ToString(), result.ToString()) + } + }) +} + +func TestGetLiquidityForAmounts(t *testing.T) { + q96 := u256.MustFromDecimal(consts.Q96) + + tests := []struct { + name string + sqrtRatioX96 string + sqrtRatioAX96 string + sqrtRatioBX96 string + amount0 string + amount1 string + expected string + }{ + { + name: "Basic Liquidity Calculation - Token0 Dominant", + sqrtRatioX96: q96.ToString(), // 현재 가격이 Q96 + sqrtRatioAX96: (new(u256.Uint).Mul(q96, u256.NewUint(4))).ToString(), + sqrtRatioBX96: (new(u256.Uint).Mul(q96, u256.NewUint(16))).ToString(), + amount0: "1000000", + amount1: "1000000", + expected: "5333333", + }, + { + name: "Within Range - Both Token0 and Token1", + sqrtRatioX96: (new(u256.Uint).Mul(q96, u256.NewUint(2))).ToString(), + sqrtRatioAX96: (new(u256.Uint).Mul(q96, u256.NewUint(4))).ToString(), + sqrtRatioBX96: (new(u256.Uint).Mul(q96, u256.NewUint(16))).ToString(), + amount0: "2000000", + amount1: "3000000", + expected: "10666666", + }, + { + name: "Token1 Dominant - Price Above Upper Bound", + sqrtRatioX96: (new(u256.Uint).Mul(q96, u256.NewUint(20))).ToString(), + sqrtRatioAX96: (new(u256.Uint).Mul(q96, u256.NewUint(4))).ToString(), + sqrtRatioBX96: (new(u256.Uint).Mul(q96, u256.NewUint(16))).ToString(), + amount0: "1500000", + amount1: "3000000", + expected: "250000", + }, + { + name: "Edge Case - sqrtRatioX96 = Lower Bound", + sqrtRatioX96: (new(u256.Uint).Mul(q96, u256.NewUint(4))).ToString(), + sqrtRatioAX96: (new(u256.Uint).Mul(q96, u256.NewUint(4))).ToString(), + sqrtRatioBX96: (new(u256.Uint).Mul(q96, u256.NewUint(16))).ToString(), + amount0: "500000", + amount1: "500000", + expected: "2666666", + }, + { + name: "Edge Case - sqrtRatioX96 = Upper Bound", + sqrtRatioX96: (new(u256.Uint).Mul(q96, u256.NewUint(16))).ToString(), + sqrtRatioAX96: (new(u256.Uint).Mul(q96, u256.NewUint(4))).ToString(), + sqrtRatioBX96: (new(u256.Uint).Mul(q96, u256.NewUint(16))).ToString(), + amount0: "1000000", + amount1: "1000000", + expected: "83333", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sqrtRatioX96 := u256.MustFromDecimal(tc.sqrtRatioX96) + sqrtRatioAX96 := u256.MustFromDecimal(tc.sqrtRatioAX96) + sqrtRatioBX96 := u256.MustFromDecimal(tc.sqrtRatioBX96) + amount0 := u256.MustFromDecimal(tc.amount0) + amount1 := u256.MustFromDecimal(tc.amount1) + + result := GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1) + expected := u256.MustFromDecimal(tc.expected) + + uassert.Equal(t, expected.ToString(), result.ToString()) + }) + } +} + +func TestComputeAmount0ForLiquidity(t *testing.T) { + q96 := u256.MustFromDecimal("79228162514264337593543950336") // 2^96 + + tests := []struct { + name string + sqrtRatioAX96 string + sqrtRatioBX96 string + liquidity string + expected string + }{ + { + name: "Basic Case - Small Range", + sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(4)).ToString(), + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(8)).ToString(), + liquidity: "1000000", + expected: "125000", + }, + { + name: "Large Liquidity - Wide Range", + sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(2)).ToString(), + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(16)).ToString(), + liquidity: "5000000000", + expected: "2187500000", + }, + { + name: "Edge Case - Equal Bounds", + sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(8)).ToString(), + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(8)).ToString(), + liquidity: "1000000", + expected: "0", + }, + { + name: "Minimum Liquidity", + sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(5)).ToString(), + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(10)).ToString(), + liquidity: "1", + expected: "0", + }, + { + name: "Max Liquidity", + sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(1)).ToString(), + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(32)).ToString(), + liquidity: "1000000000000000000", + expected: "968750000000000000", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sqrtRatioAX96 := u256.MustFromDecimal(tc.sqrtRatioAX96) + sqrtRatioBX96 := u256.MustFromDecimal(tc.sqrtRatioBX96) + liquidity := u256.MustFromDecimal(tc.liquidity) + + result := computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) + expected := u256.MustFromDecimal(tc.expected) + + if result.ToString() != expected.ToString() { + t.Errorf("expected %s but got %s", expected.ToString(), result.ToString()) + } + }) + } +} + +func TestComputeAmount1ForLiquidity(t *testing.T) { + q96 := u256.MustFromDecimal(consts.Q96) // 2^96 = 79228162514264337593543950336 + + tests := []struct { + name string + sqrtRatioAX96 *u256.Uint + sqrtRatioBX96 *u256.Uint + liquidity *u256.Uint + expectedAmount string + }{ + { + name: "Basic Case - Small Liquidity", + sqrtRatioAX96: q96, + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(4)), // sqrtRatioBX96 = 4 * Q96 + liquidity: u256.NewUint(1000000000), + expectedAmount: "3000000000", // (4-1)*liquidity = 3 * 10^9 + }, + { + name: "Edge Case - Equal Ratios", + sqrtRatioAX96: q96, + sqrtRatioBX96: q96, + liquidity: u256.NewUint(1000000), + expectedAmount: "0", + }, + { + name: "Zero Liquidity", + sqrtRatioAX96: q96, + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(2)), + liquidity: u256.Zero(), + expectedAmount: "0", + }, + { + name: "Large Liquidity", + sqrtRatioAX96: q96, + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(16)), // sqrtRatioBX96 = 16 * Q96 + liquidity: u256.NewUint(1000000000000000000), // 1e18 liquidity + expectedAmount: "15000000000000000000", // (16-1) * 1e18 = 15 * 1e18 + }, + { + name: "Descending Ratios (Order Correction)", + sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(8)), + sqrtRatioBX96: q96, + liquidity: u256.NewUint(500000), + expectedAmount: "3500000", // (8-1)*500000 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := computeAmount1ForLiquidity(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) + uassert.Equal(t, tt.expectedAmount, result.ToString(), ufmt.Sprintf("expected %s but got %s", tt.expectedAmount, result.ToString())) + }) + } +} + +func TestGetAmountsForLiquidity(t *testing.T) { + q96 := u256.MustFromDecimal(consts.Q96) // 2^96 = 79228162514264337593543950336 + + tests := []struct { + name string + sqrtRatioX96 *u256.Uint + sqrtRatioAX96 *u256.Uint + sqrtRatioBX96 *u256.Uint + liquidity *u256.Uint + expectedAmount0 string + expectedAmount1 string + }{ + { + name: "Basic Case - Within Range", + sqrtRatioX96: new(u256.Uint).Mul(q96, u256.NewUint(2)), // Current price at 2 * Q96 + sqrtRatioAX96: q96, // Lower bound at 1 * Q96 + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(4)), // Upper bound at 4 * Q96 + liquidity: u256.NewUint(1000000), + expectedAmount0: "250000", + expectedAmount1: "1000000", + }, + { + name: "Edge Case - At Lower Bound (sqrtRatioX96 == sqrtRatioAX96)", + sqrtRatioX96: q96, + sqrtRatioAX96: q96, + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(8)), + liquidity: u256.NewUint(1000000), + expectedAmount0: "875000", + expectedAmount1: "0", + }, + { + name: "Edge Case - At Upper Bound (sqrtRatioX96 == sqrtRatioBX96)", + sqrtRatioX96: new(u256.Uint).Mul(q96, u256.NewUint(8)), + sqrtRatioAX96: q96, + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(8)), + liquidity: u256.NewUint(1000000), + expectedAmount0: "0", + expectedAmount1: "7000000", + }, + { + name: "Out of Range - Below Lower Bound", + sqrtRatioX96: new(u256.Uint).Div(q96, u256.NewUint(2)), // Current price at 0.5 * Q96 + sqrtRatioAX96: q96, + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(8)), + liquidity: u256.NewUint(500000), + expectedAmount0: "437500", + expectedAmount1: "0", + }, + { + name: "Out of Range - Above Upper Bound", + sqrtRatioX96: new(u256.Uint).Mul(q96, u256.NewUint(10)), // Current price at 10 * Q96 + sqrtRatioAX96: q96, + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(8)), + liquidity: u256.NewUint(2000000), + expectedAmount0: "0", + expectedAmount1: "14000000", + }, + { + name: "Zero Liquidity", + sqrtRatioX96: q96, + sqrtRatioAX96: q96, + sqrtRatioBX96: new(u256.Uint).Mul(q96, u256.NewUint(16)), + liquidity: u256.Zero(), + expectedAmount0: "0", + expectedAmount1: "0", + }, + { + name: "Descending Ratios (Order Correction)", + sqrtRatioX96: q96, + sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(8)), + sqrtRatioBX96: q96, + liquidity: u256.NewUint(1000000), + expectedAmount0: "875000", + expectedAmount1: "0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + amount0, amount1 := GetAmountsForLiquidity(tt.sqrtRatioX96, tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) + uassert.Equal(t, tt.expectedAmount0, amount0, ufmt.Sprintf("expected %s but got %s for amount0", tt.expectedAmount0, amount0)) + uassert.Equal(t, tt.expectedAmount1, amount1, ufmt.Sprintf("expected %s but got %s for amount1", tt.expectedAmount1, amount1)) + }) + } +} diff --git a/_deploy/r/gnoswap/common/math.gno b/_deploy/r/gnoswap/common/math.gno index ae626926..ac7a67b2 100644 --- a/_deploy/r/gnoswap/common/math.gno +++ b/_deploy/r/gnoswap/common/math.gno @@ -77,7 +77,7 @@ func U256Max(x, y *u256.Uint) *u256.Uint { // If the value is greater than the maximum int256 value, it panics. func SafeConvertUint256ToInt256(x *u256.Uint) *i256.Int { if x.Gt(u256.MustFromDecimal(consts.MAX_INT256)) { - panic(addDetailToError( + panic(newErrorWithDetail( errOverflow, ufmt.Sprintf("can not convert %s to int256", x.ToString()), )) @@ -90,7 +90,7 @@ func SafeConvertUint256ToInt256(x *u256.Uint) *i256.Int { func SafeConvertUint256ToUint64(x *u256.Uint) uint64 { value, overflow := x.Uint64WithOverflow() if overflow { - panic(addDetailToError( + panic(newErrorWithDetail( errOverflow, ufmt.Sprintf("can not convert %s to uint64", x.ToString()), )) diff --git a/_deploy/r/gnoswap/common/tick_math.gno b/_deploy/r/gnoswap/common/tick_math.gno index 2a0bc689..e86b95eb 100644 --- a/_deploy/r/gnoswap/common/tick_math.gno +++ b/_deploy/r/gnoswap/common/tick_math.gno @@ -7,6 +7,23 @@ import ( u256 "gno.land/p/gnoswap/uint256" ) +const ( + MAX_UINT8 string = "255" + MAX_UINT16 string = "65535" + MAX_UINT32 string = "4294967295" + MAX_UINT64 string = "18446744073709551615" + MAX_UINT128 string = "340282366920938463463374607431768211455" + MAX_UINT160 string = "1461501637330902918203684832716283019655932542975" + MAX_UINT256 string = "115792089237316195423570985008687907853269984665640564039457584007913129639935" + MAX_INT256 string = "57896044618658097711785492504343953926634992332820282019728792003956564819967" + + Q64 string = "18446744073709551616" // 2 ** 64 + Q96 string = "79228162514264337593543950336" // 2 ** 96 + Q128 string = "340282366920938463463374607431768211456" // 2 ** 128 +) + +// TODO: +// 1. change avl.Tree var tickRatioMap = map[int32]*u256.Uint{ 0x1: u256.MustFromDecimal("340265354078544963557816517032075149313"), // 0xfffcb933bd6fad37aa2d162d1a594001, 0x2: u256.MustFromDecimal("340248342086729790484326174814286782778"), // 0xfff97272373d413259a46990580e213a, @@ -46,54 +63,85 @@ var ( maxTick = int32(887272) ) +// TickMathGetSqrtRatioAtTick calculates the square root price ratio for a given tick. +// +// This function computes the square root ratio (sqrt(price)) at a specific tick, +// using a precomputed mapping of ratios. The result is returned as a 160-bit +// fixed-point value (Q64.96 format). +// +// Parameters: +// - tick (int32): The tick index for which the square root ratio is calculated. +// +// Returns: +// - *u256.Uint: The square root price ratio at the given tick, represented as a 160-bit unsigned integer. +// +// Behavior: +// 1. Validates that the tick is within the acceptable range by asserting its absolute value. +// 2. Initializes the ratio based on whether the least significant bit of the tick is set, using a lookup table (`tickRatioMap`). +// 3. Iteratively adjusts the ratio by multiplying and right-shifting with precomputed values for each relevant bit set in the tick value. +// 4. If the tick is positive, the ratio is inverted by dividing a maximum uint256 value by the computed ratio. +// 5. The result is split into upper 128 bits and lower 32 bits for precision handling. +// 6. If the lower 32 bits are non-zero, the upper 128 bits are incremented by 1 to ensure rounding up. +// +// Example: +// - For a tick of `0`, the ratio represents `1.0000` in Q64.96 format. +// - For a tick of `-887272` (minimum tick), the ratio represents the smallest possible price. +// - For a tick of `887272` (maximum tick), the ratio represents the highest possible price. +// +// Panics: +// - If the absolute tick value exceeds the maximum allowed tick range. +// +// Notes: +// - The function relies on a precomputed map `tickRatioMap` to optimize calculations. +// - Handles rounding by adding 1 if the remainder of the division is non-zero. func TickMathGetSqrtRatioAtTick(tick int32) *u256.Uint { // uint160 sqrtPriceX96 absTick := abs(tick) - if absTick > maxTick { - panic(addDetailToError( - errOutOfRange, - ufmt.Sprintf("tick is out of range (larger than 887272), tick: %d", tick), - )) - } + assertValidTickRange(absTick) - var initialBit int32 = 0x1 - var ratio *u256.Uint - if (absTick & initialBit) != 0 { - ratio = tickRatioMap[initialBit] - } else { - ratio = u256.MustFromDecimal("340282366920938463463374607431768211456") // consts.Q128 + ratio := u256.MustFromDecimal("340282366920938463463374607431768211456") // consts.Q128 + initialBit := int32(0x1) + if val, exists := tickRatioMap[initialBit]; (absTick&initialBit) != 0 && exists { + ratio = val } for mask, value := range tickRatioMap { - if (mask != initialBit) && absTick&mask != 0 { + if mask == initialBit { + continue + } + + if absTick&mask != 0 { // ratio = (ratio * value) >> 128 - ratio = ratio.Mul(ratio, value) + ratio = new(u256.Uint).Mul(ratio, value) ratio = ratio.Rsh(ratio, 128) } } if tick > 0 { - maxUint256 := u256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935") // consts.MAX_UINT256 + maxUint256 := u256.MustFromDecimal(MAX_UINT256) // consts.MAX_UINT256 + if ratio.IsZero() { + return u256.Zero() + } ratio = new(u256.Uint).Div(maxUint256, ratio) } - shifted := new(u256.Uint).Rsh(ratio, 32) // ratio >> 32 - remainder := ratio.Mod(ratio, shift1By32Left) // ratio % (1 << 32) + upper128Bits := new(u256.Uint).Rsh(ratio, 32) // ratio >> 32 + lower32Bits := ratio.Mod(ratio, shift1By32Left) // ratio % (1 << 32) - var adj *u256.Uint - if remainder.IsZero() { - adj = u256.Zero() + var roundUp *u256.Uint + if lower32Bits.IsZero() { + roundUp = u256.Zero() } else { - adj = u256.One() + roundUp = u256.One() } - return new(u256.Uint).Add(shifted, adj) + return new(u256.Uint).Add(upper128Bits, roundUp) } func TickMathGetTickAtSqrtRatio(sqrtPriceX96 *u256.Uint) int32 { cond1 := sqrtPriceX96.Gte(u256.MustFromDecimal("4295128739")) // MIN_SQRT_RATIO cond2 := sqrtPriceX96.Lt(u256.MustFromDecimal("1461446703485210103287273052203988822378723970342")) // MAX_SQRT_RATIO if !(cond1 && cond2) { - panic(addDetailToError( + panic(newErrorWithDetail( errOutOfRange, ufmt.Sprintf("tick_math.gno__TickMathGetTickAtSqrtRatio() || sqrtPriceX96 is out of range, sqrtPriceX96: %s", sqrtPriceX96.ToString()), )) @@ -219,6 +267,7 @@ func gt(x, y *u256.Uint) *u256.Uint { return u256.Zero() } +// abs returns the absolute value of the given integer. func abs(x int32) int32 { if x < 0 { return -x @@ -226,3 +275,13 @@ func abs(x int32) int32 { return x } + +// assertValidTickRange validates that the absolute tick value is within the acceptable range. +func assertValidTickRange(absTick int32) { + if absTick > maxTick { + panic(newErrorWithDetail( + errOutOfRange, + ufmt.Sprintf("abs tick is out of range (larger than 887272), abs tick: %d", absTick), + )) + } +} diff --git a/_deploy/r/gnoswap/common/tests/tick_math_test.gno b/_deploy/r/gnoswap/common/tick_math_test.gno similarity index 95% rename from _deploy/r/gnoswap/common/tests/tick_math_test.gno rename to _deploy/r/gnoswap/common/tick_math_test.gno index c97ad6fb..d0755532 100644 --- a/_deploy/r/gnoswap/common/tests/tick_math_test.gno +++ b/_deploy/r/gnoswap/common/tick_math_test.gno @@ -22,7 +22,7 @@ func TestTickMathGetSqrtRatioAtTick(t *testing.T) { uassert.PanicsWithMessage( t, - "[GNOSWAP-COMMON-003] value out of range || tick_math.gno__TickMathGetSqrtRatioAtTick() || tick is out of range (larger than 887272), tick: -887273", + "[GNOSWAP-COMMON-003] value out of range || abs tick is out of range (larger than 887272), abs tick: 887273", func() { TickMathGetSqrtRatioAtTick(tick) }, @@ -34,7 +34,7 @@ func TestTickMathGetSqrtRatioAtTick(t *testing.T) { uassert.PanicsWithMessage( t, - "[GNOSWAP-COMMON-003] value out of range || tick_math.gno__TickMathGetSqrtRatioAtTick() || tick is out of range (larger than 887272), tick: 887273", + "[GNOSWAP-COMMON-003] value out of range || abs tick is out of range (larger than 887272), abs tick: 887273", func() { TickMathGetSqrtRatioAtTick(tick) }, diff --git a/_deploy/r/gnoswap/common/util.gno b/_deploy/r/gnoswap/common/util.gno index a8689214..1fe2f40a 100644 --- a/_deploy/r/gnoswap/common/util.gno +++ b/_deploy/r/gnoswap/common/util.gno @@ -2,9 +2,32 @@ package common import ( "std" + + u256 "gno.land/p/gnoswap/uint256" ) -func getPrev() (string, string) { - prev := std.PrevRealm() +// assertOnlyNotNil panics if the value is nil. +func assertOnlyNotNil(value *u256.Uint) { + if value == nil { + panic(newErrorWithDetail( + errInvalidInput, + "value is nil", + )) + } +} + +// getPrevRealm returns object of the previous realm. +func getPrevRealm() std.Realm { + return std.PrevRealm() +} + +// getPrevAddr returns the address of the previous realm. +func getPrevAddr() std.Address { + return std.PrevRealm().Addr() +} + +// getPrevAsString returns the address and package path of the previous realm. +func getPrevAsString() (string, string) { + prev := getPrevRealm() return prev.Addr().String(), prev.PkgPath() } diff --git a/_deploy/r/gnoswap/consts/consts.gno b/_deploy/r/gnoswap/consts/consts.gno index d2ebff66..c4a3a897 100644 --- a/_deploy/r/gnoswap/consts/consts.gno +++ b/_deploy/r/gnoswap/consts/consts.gno @@ -18,6 +18,7 @@ const ( // WRAP & UNWRAP const ( GNOT string = "gnot" + UGNOT string = "ugnot" WRAPPED_WUGNOT string = "gno.land/r/demo/wugnot" // defined in https://github.com/gnolang/gno/blob/81a88a2976ba9f2f9127ebbe7fb7d1e1f7fa4bd4/examples/gno.land/r/demo/wugnot/wugnot.gno#L19 @@ -111,7 +112,9 @@ const ( Q96 string = "79228162514264337593543950336" // 2 ** 96 Q128 string = "340282366920938463463374607431768211456" // 2 ** 128 + Q96_RESOLUTION uint = 96 Q128_RESOLUTION uint = 128 + Q160_RESOLUTION uint = 160 ) // TIMESTAMP & DAY