Skip to content

Commit

Permalink
Merge pull request #220 from gnoswap-labs/GSW-1071-fix-unclaimed-fee-…
Browse files Browse the repository at this point in the history
…overflow-underflow

GSW-1071 fix unclaimed fee overflow underflow
  • Loading branch information
notJoon committed May 16, 2024
2 parents 4283001 + 1f7bf37 commit 48b50a0
Show file tree
Hide file tree
Showing 4 changed files with 377 additions and 39 deletions.
81 changes: 53 additions & 28 deletions position/_RPC_api.gno
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
pl "gno.land/r/demo/pool"

i256 "gno.land/p/demo/gnoswap/int256"
u256 "gno.land/p/demo/gnoswap/uint256"
)

type RpcPosition struct {
Expand Down Expand Up @@ -123,8 +122,8 @@ func rpcMakePosition(lpTokenId uint64) RpcPosition {
i256.FromUint256(position.liquidity),
)

unclaimedFee0 := u256.Zero()
unclaimedFee1 := u256.Zero()
unclaimedFee0 := i256.Zero()
unclaimedFee1 := i256.Zero()
if !burned {
unclaimedFee0, unclaimedFee1 = unclaimedFee(lpTokenId)
}
Expand All @@ -148,10 +147,12 @@ func rpcMakePosition(lpTokenId uint64) RpcPosition {
}
}

func unclaimedFee(tokenId uint64) (*u256.Uint, *u256.Uint) {
func unclaimedFee(tokenId uint64) (*i256.Int, *i256.Int) {
// ref: https://blog.uniswap.org/uniswap-v3-math-primer-2#calculating-uncollected-fees

liquidity := positions[tokenId].liquidity
_liquidity := positions[tokenId].liquidity // u256
liquidity := i256.FromUint256(_liquidity) // i256

tickLower := positions[tokenId].tickLower
tickUpper := positions[tokenId].tickUpper

Expand All @@ -160,23 +161,35 @@ func unclaimedFee(tokenId uint64) (*u256.Uint, *u256.Uint) {

currentTick := pool.PoolGetSlot0Tick()

feeGrowthGlobal0X128 := pool.PoolGetFeeGrowthGlobal0X128()
feeGrowthGlobal1X128 := pool.PoolGetFeeGrowthGlobal1X128()
_feeGrowthGlobal0X128 := pool.PoolGetFeeGrowthGlobal0X128() // u256
feeGrowthGlobal0X128 := i256.FromUint256(_feeGrowthGlobal0X128) // i256

_feeGrowthGlobal1X128 := pool.PoolGetFeeGrowthGlobal1X128() // u256
feeGrowthGlobal1X128 := i256.FromUint256(_feeGrowthGlobal1X128) // i256

_tickUpperFeeGrowthOutside0X128 := pool.PoolGetTickFeeGrowthOutside0X128(tickUpper) // u256
tickUpperFeeGrowthOutside0X128 := i256.FromUint256(_tickUpperFeeGrowthOutside0X128) // i256

_tickUpperFeeGrowthOutside1X128 := pool.PoolGetTickFeeGrowthOutside1X128(tickUpper) // u256
tickUpperFeeGrowthOutside1X128 := i256.FromUint256(_tickUpperFeeGrowthOutside1X128) // i256

_tickLowerFeeGrowthOutside0X128 := pool.PoolGetTickFeeGrowthOutside0X128(tickLower) // u256
tickLowerFeeGrowthOutside0X128 := i256.FromUint256(_tickLowerFeeGrowthOutside0X128) // i256

tickUpperFeeGrowthOutside0X128 := pool.PoolGetTickFeeGrowthOutside0X128(tickUpper)
tickUpperFeeGrowthOutside1X128 := pool.PoolGetTickFeeGrowthOutside1X128(tickUpper)
_tickLowerFeeGrowthOutside1X128 := pool.PoolGetTickFeeGrowthOutside1X128(tickLower) // u256
tickLowerFeeGrowthOutside1X128 := i256.FromUint256(_tickLowerFeeGrowthOutside1X128) // i256

tickLowerFeeGrowthOutside0X128 := pool.PoolGetTickFeeGrowthOutside0X128(tickLower)
tickLowerFeeGrowthOutside1X128 := pool.PoolGetTickFeeGrowthOutside1X128(tickLower)
_feeGrowthInside0LastX128 := positions[tokenId].feeGrowthInside0LastX128 // u256
feeGrowthInside0LastX128 := i256.FromUint256(_feeGrowthInside0LastX128) // i256

feeGrowthInside0LastX128 := positions[tokenId].feeGrowthInside0LastX128
feeGrowthInside1LastX128 := positions[tokenId].feeGrowthInside1LastX128
_feeGrowthInside1LastX128 := positions[tokenId].feeGrowthInside1LastX128 // u256
feeGrowthInside1LastX128 := i256.FromUint256(_feeGrowthInside1LastX128) // i256

var tickLowerFeeGrowthBelow0, tickLowerFeeGrowthBelow1, tickUpperFeeGrowthAbove0, tickUpperFeeGrowthAbove1 *u256.Uint
var tickLowerFeeGrowthBelow0, tickLowerFeeGrowthBelow1, tickUpperFeeGrowthAbove0, tickUpperFeeGrowthAbove1 *i256.Int

if currentTick >= tickUpper {
tickUpperFeeGrowthAbove0 = new(u256.Uint).Sub(feeGrowthGlobal0X128, tickUpperFeeGrowthOutside0X128)
tickUpperFeeGrowthAbove1 = new(u256.Uint).Sub(feeGrowthGlobal1X128, tickUpperFeeGrowthOutside1X128)
tickUpperFeeGrowthAbove0 = subIn256(feeGrowthGlobal0X128, tickUpperFeeGrowthOutside0X128)
tickUpperFeeGrowthAbove1 = subIn256(feeGrowthGlobal1X128, tickUpperFeeGrowthOutside1X128)
} else {
tickUpperFeeGrowthAbove0 = tickUpperFeeGrowthOutside0X128
tickUpperFeeGrowthAbove1 = tickUpperFeeGrowthOutside1X128
Expand All @@ -186,27 +199,39 @@ func unclaimedFee(tokenId uint64) (*u256.Uint, *u256.Uint) {
tickLowerFeeGrowthBelow0 = tickLowerFeeGrowthOutside0X128
tickLowerFeeGrowthBelow1 = tickLowerFeeGrowthOutside1X128
} else {
tickLowerFeeGrowthBelow0 = new(u256.Uint).Sub(feeGrowthGlobal0X128, tickLowerFeeGrowthOutside0X128)
tickLowerFeeGrowthBelow1 = new(u256.Uint).Sub(feeGrowthGlobal1X128, tickLowerFeeGrowthOutside1X128)
tickLowerFeeGrowthBelow0 = subIn256(feeGrowthGlobal0X128, tickLowerFeeGrowthOutside0X128)
tickLowerFeeGrowthBelow1 = subIn256(feeGrowthGlobal1X128, tickLowerFeeGrowthOutside1X128)
}

feeGrowthInside0X128 := new(u256.Uint).Sub(feeGrowthGlobal0X128, tickLowerFeeGrowthBelow0)
feeGrowthInside0X128 = new(u256.Uint).Sub(feeGrowthInside0X128, tickUpperFeeGrowthAbove0)
feeGrowthInside0X128 := subIn256(feeGrowthGlobal0X128, tickLowerFeeGrowthBelow0)
feeGrowthInside0X128 = subIn256(feeGrowthInside0X128, tickUpperFeeGrowthAbove0)

feeGrowthInside1X128 := new(u256.Uint).Sub(feeGrowthGlobal1X128, tickLowerFeeGrowthBelow1)
feeGrowthInside1X128 = new(u256.Uint).Sub(feeGrowthInside1X128, tickUpperFeeGrowthAbove1)
feeGrowthInside1X128 := subIn256(feeGrowthGlobal1X128, tickLowerFeeGrowthBelow1)
feeGrowthInside1X128 = subIn256(feeGrowthInside1X128, tickUpperFeeGrowthAbove1)

value01 := new(u256.Uint).Sub(feeGrowthInside0X128, feeGrowthInside0LastX128)
value02 := new(u256.Uint).Mul(liquidity, value01)
unclaimedFee0 := new(u256.Uint).Div(value02, u256.MustFromDecimal(consts.Q128))
value01 := subIn256(feeGrowthInside0X128, feeGrowthInside0LastX128)
value02 := i256.Zero().Mul(liquidity, value01)
unclaimedFee0 := i256.Zero().Div(value02, i256.MustFromDecimal(consts.Q128))

value11 := new(u256.Uint).Sub(feeGrowthInside1X128, feeGrowthInside1LastX128)
value12 := new(u256.Uint).Mul(liquidity, value11)
unclaimedFee1 := new(u256.Uint).Div(value12, u256.MustFromDecimal(consts.Q128))
value11 := subIn256(feeGrowthInside1X128, feeGrowthInside1LastX128)
value12 := i256.Zero().Mul(liquidity, value11)
unclaimedFee1 := i256.Zero().Div(value12, i256.MustFromDecimal(consts.Q128))

return unclaimedFee0, unclaimedFee1
}

func subIn256(x, y *i256.Int) *i256.Int {
value := i256.Zero()
diff := value.Sub(x, y)

if diff.IsNeg() {
q256 := i256.MustFromDecimal(consts.MAX_UINT256)
return diff.Add(diff, q256)
}

return diff
}

func isBurned(tokenId uint64) bool {
return positions[tokenId].burned
}
227 changes: 227 additions & 0 deletions position/_TEST_/_TEST_position_ZZ_increase_decrease_native_test.gnoA
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package position

import (
"std"
"testing"

"gno.land/r/demo/gnoswap/common"
"gno.land/r/demo/gnoswap/consts"

"gno.land/r/demo/gns"
"gno.land/r/demo/wugnot"

pl "gno.land/r/demo/pool"
)

// 1. Create Pool
func TestPoolInitCreatePool(t *testing.T) {
std.TestSetPrevAddr(gsa)

gns.Approve(a2u(consts.POOL_ADDR), consts.POOL_CREATION_FEE)
pl.CreatePool(consts.GNS_PATH, consts.GNOT, fee500, common.TickMathGetSqrtRatioAtTick(10000).ToString()) // x2.71814592682522526700950038502924144268035888671875
// event: {GNOSWAP gno.land/r/demo/pool CreatePool [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm } {p_poolPath gno.land/r/demo/gns:gno.land/r/demo/wugnot:500}]}
}

func TestMintPosition(t *testing.T) {
std.TestSetPrevAddr(gsa)

gns.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX)
wugnot.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX)

wugnot.Approve(a2u(consts.POSITION_ADDR), consts.UINT64_MAX) // WRAP

// prepare 50000005ugnot (5 for refund test)
testBanker := std.GetBanker(std.BankerTypeRealmIssue)
testBanker.IssueCoin(gsa, "ugnot", 50000005)

// simulate transfer & decrase
gsaNativeBalance := ugnotBalanceOf(gsa)
shouldEQ(t, gsaNativeBalance, 50000005)

std.TestSetOrigSend(std.Coins{{"ugnot", 50000005}}, nil)
testBanker.RemoveCoin(std.GetOrigCaller(), "ugnot", -50000005)

gsaNativeBalance = ugnotBalanceOf(gsa)
shouldEQ(t, gsaNativeBalance, 0)

gsaOldWugnotBalance := wugnot.BalanceOf(a2u(gsa))
shouldEQ(t, gsaOldWugnotBalance, 0)

tokenId, liquidity, amount0, amount1 := Mint(
consts.GNS_PATH,
consts.GNOT,
fee500,
8000,
12000,
"50000000",
"50000000",
"0",
"0",
max_timeout,
gsa.String(),
)
// event: {GNOSWAP gno.land/r/demo/position Mint [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm } {p_poolPath gno.land/r/demo/gns:gno.land/r/demo/wugnot:500} {p_tickLower 8000} {p_tickUpper 12000} {tokenId 1} {liquidity 318704392} {amount0 18394892} {amount1 50000000}]}

shouldEQ(t, tokenId, 1)
shouldEQ(t, getNextId(), 2)
shouldEQ(t, amount0, "18394892")
shouldEQ(t, amount1, "50000000")

position := positions[tokenId]
shouldEQ(t, position.poolKey, "gno.land/r/demo/gns:gno.land/r/demo/wugnot:500")

// SPEND ALL WUGNOT
newOldWugnotBalance := wugnot.BalanceOf(a2u(gsa))
shouldEQ(t, gsaOldWugnotBalance, newOldWugnotBalance)

gsaNativeBalance = ugnotBalanceOf(gsa)
shouldEQ(t, gsaNativeBalance, 5)
// 1. 50000005 ugnot sent
// 2. 50000005 ugnot wrapped to wugnot
// 3. 50000000 wugnot spent to mint (amount1)
// 4. refund 50000005 - 50000000 = 5

}

func TestIncreaseLiquidity(t *testing.T) {
std.TestSetPrevAddr(gsa)

gns.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX)
wugnot.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX)

wugnot.Approve(a2u(consts.POSITION_ADDR), consts.UINT64_MAX) // WRAP

pool := getPoolFromLpTokenId(uint64(1))
oldLiquidity := pool.PoolGetLiquidity()

// prepare 10000005ugnot (5 for refund test)
testBanker := std.GetBanker(std.BankerTypeRealmIssue)
testBanker.IssueCoin(gsa, "ugnot", 10000005)

// simulate transfer & decrase
gsaNativeBalance := ugnotBalanceOf(gsa)
shouldEQ(t, gsaNativeBalance, 10000010)

std.TestSetOrigSend(std.Coins{{"ugnot", 10000005}}, nil)
testBanker.RemoveCoin(std.GetOrigCaller(), "ugnot", -10000005)

gsaNativeBalance = ugnotBalanceOf(gsa)
shouldEQ(t, gsaNativeBalance, 5)

gsaOldWugnotBalance := wugnot.BalanceOf(a2u(gsa))
shouldEQ(t, gsaOldWugnotBalance, 0)

_, _, m0, m1, _ := IncreaseLiquidity( // tokenId, liq, a0, a1, poolPath
uint64(1), // tokenId
"10000000", // amount0Desired
"10000000", // amount1Desired
"0", // amount0Min
"0", // amount1Min
max_timeout, // deadline
)
// event: {GNOSWAP gno.land/r/demo/position IncreaseLiquidity [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm } {p_tokenId 1} {poolPath gno.land/r/demo/gns:gno.land/r/demo/wugnot:500} {liquidity 63740878} {amount0 3678979} {amount1 10000000}]}
shouldEQ(t, m0, "3678979")
shouldEQ(t, m1, "10000000")

newLiquidity := pool.PoolGetLiquidity()

shouldEQ(t, newLiquidity.Gt(oldLiquidity), true)

// SPEND ALL WUGNOT
newOldWugnotBalance := wugnot.BalanceOf(a2u(gsa))
shouldEQ(t, gsaOldWugnotBalance, newOldWugnotBalance)

gsaNativeBalance = ugnotBalanceOf(gsa)
shouldEQ(t, gsaNativeBalance, 10)
// 1. 10000005 ugnot sent
// 2. 10000005 ugnot wrapped to wugnot
// 3. 10000000 wugnot spent to mint (amount1)
// 4. refund 10000005 - 10000000 = 5
// 5. user already had 5 ugnot = 5 + 5
}

func TestDecreaseLiquidityWrapped(t *testing.T) {
std.TestSetPrevRealm("")
std.TestSetOrigCaller(gsa)

oldLiquidity := getPoolFromLpTokenId(uint64(1)).PoolGetLiquidity()

userWugnotBalance := wugnot.BalanceOf(a2u(gsa))
shouldEQ(t, userWugnotBalance, 0)

userUgnotBalance := ugnotBalanceOf(gsa)
shouldEQ(t, userUgnotBalance, 10)

_, _, _, _, a0, a1, _ := DecreaseLiquidity( // tokenId, liquidity, fee0, fee1, amount0, amount1, poolPath
uint64(1), // tokenId
20, // liquidityRatio
"0", // amount0Min
"0", // amount1Min
max_timeout, // deadline
false, // unwrapResult
)
// --- event: {GNOSWAP gno.land/r/demo/pool HandleWithdrawalFee [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm gno.land/r/demo/position} {p_tokenId 1} {p_token0Path gno.land/r/demo/gns} {p_token1Path gno.land/r/demo/wugnot} {fee0Amount 0} {fee1Amount 0}]}
// --- event: {GNOSWAP gno.land/r/demo/position CollectFee [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm } {p_tokenId 1} {fee0 0} {fee1 0} {poolPath gno.land/r/demo/gns:gno.land/r/demo/wugnot:500}]}
// --- event: {GNOSWAP gno.land/r/demo/position DecreaseLiquidity [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm } {p_tokenId 1} {p_liquidityRatio 20} {poolPath gno.land/r/demo/gns:gno.land/r/demo/wugnot:500} {liquidity 76489054} {fee0 0} {fee1 0} {amount0 4414773} {amount1 11999999}]}

userWugnotBalance = wugnot.BalanceOf(a2u(gsa)) // wrapped result, so wunogt increased
shouldEQ(t, userWugnotBalance, 11999999)

userUgnotBalance = ugnotBalanceOf(gsa) // wrapped result, so ugnot didn't change
shouldEQ(t, userUgnotBalance, 10)

newLiquidity := getPoolFromLpTokenId(uint64(1)).PoolGetLiquidity()
shouldEQ(t, true, newLiquidity.Lt(oldLiquidity))

// check fee left
tokenId, fee0, fee1, poolPath := CollectFee(1)
// --- event: {GNOSWAP gno.land/r/demo/pool HandleWithdrawalFee [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm gno.land/r/demo/position} {p_tokenId 1} {p_token0Path gno.land/r/demo/gns} {p_token1Path gno.land/r/demo/wugnot} {fee0Amount 0} {fee1Amount 0}]}
// --- event: {GNOSWAP gno.land/r/demo/position CollectFee [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm } {p_tokenId 1} {fee0 0} {fee1 0} {poolPath gno.land/r/demo/gns:gno.land/r/demo/wugnot:500}]}

shouldEQ(t, tokenId, uint64(1))
shouldEQ(t, fee0, "0")
shouldEQ(t, fee1, "0")
}

func TestDecreaseLiquidityUnwrapped(t *testing.T) {
std.TestSetPrevRealm("")
std.TestSetOrigCaller(gsa)

oldLiquidity := getPoolFromLpTokenId(uint64(1)).PoolGetLiquidity()

userWugnotBalance := wugnot.BalanceOf(a2u(gsa))
shouldEQ(t, userWugnotBalance, 11999999)

userUgnotBalance := ugnotBalanceOf(gsa)
shouldEQ(t, userUgnotBalance, 10)

_, _, _, _, a0, a1, _ := DecreaseLiquidity( // tokenId, liquidity, fee0, fee1, amount0, amount1, poolPath
uint64(1), // tokenId
50, // liquidityRatio
"0", // amount0Min
"0", // amount1Min
max_timeout, // deadline
true, // unwrapResult
)
// --- event: {GNOSWAP gno.land/r/demo/pool HandleWithdrawalFee [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm gno.land/r/demo/position} {p_tokenId 1} {p_token0Path gno.land/r/demo/gns} {p_token1Path gno.land/r/demo/wugnot} {fee0Amount 0} {fee1Amount 0}]}
// --- event: {GNOSWAP gno.land/r/demo/position CollectFee [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm } {p_tokenId 1} {fee0 0} {fee1 0} {poolPath gno.land/r/demo/gns:gno.land/r/demo/wugnot:500}]}
// --- event: {GNOSWAP gno.land/r/demo/position DecreaseLiquidity [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm } {p_tokenId 1} {p_liquidityRatio 20} {poolPath gno.land/r/demo/gns:gno.land/r/demo/wugnot:500} {liquidity 76489054} {fee0 0} {fee1 0} {amount0 4414773} {amount1 11999999}]}

userWugnotBalance = wugnot.BalanceOf(a2u(gsa)) // unwrapped result, so wugnot didn't change
shouldEQ(t, userWugnotBalance, 11999999)

userUgnotBalance = ugnotBalanceOf(gsa) // unwrapped result, so ugnot decreased
shouldEQ(t, userUgnotBalance, 24000009)

newLiquidity := getPoolFromLpTokenId(uint64(1)).PoolGetLiquidity()
shouldEQ(t, true, newLiquidity.Lt(oldLiquidity))

// check fee left
tokenId, fee0, fee1, poolPath := CollectFee(1)
// --- event: {GNOSWAP gno.land/r/demo/pool HandleWithdrawalFee [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm gno.land/r/demo/position} {p_tokenId 1} {p_token0Path gno.land/r/demo/gns} {p_token1Path gno.land/r/demo/wugnot} {fee0Amount 0} {fee1Amount 0}]}
// --- event: {GNOSWAP gno.land/r/demo/position CollectFee [{m_origCaller g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq} {m_prevRealm } {p_tokenId 1} {fee0 0} {fee1 0} {poolPath gno.land/r/demo/gns:gno.land/r/demo/wugnot:500}]}

shouldEQ(t, tokenId, uint64(1))
shouldEQ(t, fee0, "0")
shouldEQ(t, fee1, "0")
}
Loading

0 comments on commit 48b50a0

Please sign in to comment.