Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion contract/r/gnoswap/v1/gnft/gnft.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"gno.land/p/demo/grc/grc721"
"gno.land/p/demo/ownable"
"gno.land/p/demo/ufmt"
"gno.land/r/gnoswap/access"
"gno.land/r/gnoswap/halt"
)

Expand Down Expand Up @@ -99,10 +100,13 @@ func SetTokenURI(cur realm, tid grc721.TokenID, tURI grc721.TokenURI) (bool, err
// - tid: token ID to transfer
//
// Returns error if transfer fails.
// Only callable when not halted.
// Only callable by staker contract.
func SafeTransferFrom(cur realm, from, to std.Address, tid grc721.TokenID) error {
halt.AssertIsNotHaltedPosition()

caller := std.PreviousRealm().Address()
access.AssertIsStaker(caller)

assertFromIsValidAddress(from)
assertToIsValidAddress(to)

Expand All @@ -117,9 +121,15 @@ func SafeTransferFrom(cur realm, from, to std.Address, tid grc721.TokenID) error
// - from: current owner address
// - to: recipient address
// - tid: token ID
//
// Returns error if transfer fails.
// Only callable by staker contract.
func TransferFrom(cur realm, from, to std.Address, tid grc721.TokenID) error {
halt.AssertIsNotHaltedPosition()

caller := std.PreviousRealm().Address()
access.AssertIsStaker(caller)

assertFromIsValidAddress(from)
assertToIsValidAddress(to)

Expand Down
64 changes: 49 additions & 15 deletions contract/r/gnoswap/v1/gnft/gnft_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ var (
positionPath = "gno.land/r/gnoswap/v1/position"
positionRealm = std.NewCodeRealm(positionPath)

stakerPath = "gno.land/r/gnoswap/v1/staker"
stakerRealm = std.NewCodeRealm(stakerPath)

addr01 = testutils.TestAddress("addr01")
addr01Realm = std.NewUserRealm(addr01)

Expand Down Expand Up @@ -305,7 +308,7 @@ func TestTransferFrom(t *testing.T) {
}{
{
name: "transfer non-existent token id",
callerRealm: std.NewUserRealm(addr01),
callerRealm: stakerRealm,
fromAddr: addr01,
toAddr: addr02,
tokenIdToTransfer: 99,
Expand All @@ -314,34 +317,38 @@ func TestTransferFrom(t *testing.T) {
},
{
name: "transfer token owned by other user without approval",
callerRealm: std.NewUserRealm(addr02),
callerRealm: stakerRealm,
fromAddr: addr01,
toAddr: addr02,
tokenIdToTransfer: 1,
shouldPanic: true,
panicMsg: "caller is not token owner or approved || caller g1q646ctzhvn60v492x8ucvyqnrj2w30cwh6efk5 is not owner g1v9jxgu3sx9047h6lta047h6lta047h6l0js7st or approved for token 1",
panicMsg: "caller is not token owner or approved || caller g1cceshmzzlmrh7rr3z30j2t5mrvsq9yccysw9nu is not owner g1v9jxgu3sx9047h6lta047h6lta047h6l0js7st or approved for token 1",
},
{
name: "transfer token owned by other user with approval",
setup: func(cur realm) {
testing.SetRealm(addr01Realm)
Approve(cross, addr02, tid(1))
Approve(cross, stakerRealm.Address(), tid(1))
},
callerRealm: std.NewUserRealm(addr02),
callerRealm: stakerRealm,
fromAddr: addr01,
toAddr: addr02,
tokenIdToTransfer: 1,
},
{
name: "transfer token owned by caller",
callerRealm: std.NewUserRealm(addr02),
name: "transfer token owned by caller",
setup: func(cur realm) {
testing.SetRealm(addr02Realm)
Approve(cross, stakerRealm.Address(), tid(1))
},
callerRealm: stakerRealm,
fromAddr: addr02,
toAddr: addr01,
tokenIdToTransfer: 1,
},
{
name: "transfer from is invalid address",
callerRealm: std.NewUserRealm(addr01),
callerRealm: stakerRealm,
fromAddr: std.Address(""),
toAddr: addr02,
tokenIdToTransfer: 1,
Expand All @@ -350,13 +357,22 @@ func TestTransferFrom(t *testing.T) {
},
{
name: "transfer to is invalid address",
callerRealm: std.NewUserRealm(addr01),
callerRealm: stakerRealm,
fromAddr: addr01,
toAddr: std.Address("this_is_invalid_address"),
tokenIdToTransfer: 1,
shouldPanic: true,
panicMsg: "[GNOSWAP-GNFT-004] invalid addresss || to address (this_is_invalid_address)",
},
{
name: "not-staker contract transfer",
callerRealm: addr01Realm,
fromAddr: addr01,
toAddr: addr02,
tokenIdToTransfer: 1,
shouldPanic: true,
panicMsg: "unauthorized: caller g1v9jxgu3sx9047h6lta047h6lta047h6l0js7st is not staker",
},
}

for _, tt := range tests {
Expand All @@ -365,6 +381,7 @@ func TestTransferFrom(t *testing.T) {
tt.setup(cross)
}

testing.SetRealm(tt.callerRealm)
if tt.shouldPanic {
uassert.AbortsWithMessage(t, tt.panicMsg, func() {
TransferFrom(cross, tt.fromAddr, tt.toAddr, tid(tt.tokenIdToTransfer))
Expand Down Expand Up @@ -600,16 +617,24 @@ func TestSafeTransferFrom(t *testing.T) {
panicMsg string
}{
{
name: "owner transfers own token",
callerRealm: addr01Realm,
name: "owner transfers own token",
setup: func(cur realm) {
testing.SetRealm(addr01Realm)
Approve(cross, stakerRealm.Address(), tid(1))
},
callerRealm: stakerRealm,
from: addr01,
to: addr02,
tokenId: 1,
shouldPanic: false,
},
{
name: "non-owner transfers without approval",
callerRealm: addr02Realm,
name: "non-owner transfers without approval",
setup: func(cur realm) {
testing.SetRealm(addr02Realm)
Approve(cross, stakerRealm.Address(), tid(1))
},
callerRealm: stakerRealm,
from: addr01,
to: addr02,
tokenId: 1,
Expand All @@ -618,7 +643,7 @@ func TestSafeTransferFrom(t *testing.T) {
},
{
name: "transfer non-existent token",
callerRealm: addr01Realm,
callerRealm: stakerRealm,
from: addr01,
to: addr02,
tokenId: 999,
Expand All @@ -627,13 +652,22 @@ func TestSafeTransferFrom(t *testing.T) {
},
{
name: "transfer to zero address",
callerRealm: addr01Realm,
callerRealm: stakerRealm,
from: addr01,
to: std.Address(""),
tokenId: 1,
shouldPanic: true,
panicMsg: "[GNOSWAP-GNFT-004] invalid addresss || to address ()",
},
{
name: "not-staker contract transfer",
callerRealm: addr01Realm,
from: addr01,
to: addr02,
tokenId: 1,
shouldPanic: true,
panicMsg: "unauthorized: caller g1v9jxgu3sx9047h6lta047h6lta047h6l0js7st is not staker",
},
}

for _, tt := range tests {
Expand Down
26 changes: 23 additions & 3 deletions contract/r/gnoswap/v1/position/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,62 +15,79 @@ Each liquidity position is a unique GRC721 NFT containing pool identifier, price
## Core Functions

### `Mint`

Creates new position NFT with initial liquidity.

- Validates tick range alignment
- Calculates optimal token ratio
- Returns actual amounts used

### `IncreaseLiquidity`

Adds liquidity to existing position.

- Maintains same price range
- Pro-rata token amounts

### `DecreaseLiquidity`

Removes liquidity while keeping NFT.

- Two-step: decrease then collect
- Calculates owed tokens

### `CollectFee`

Claims accumulated swap fees.

- No liquidity removal required
- 1% protocol fees applied

### `Reposition`

Atomically moves liquidity to new range.

- Burns old position
- Creates new position
- Mints new NFT

## Technical Details

### Tick Alignment

Ticks must align with pool's tick spacing:

```
0.01% fee: every 1 tick
0.05% fee: every 10 ticks
0.05% fee: every 10 ticks
0.3% fee: every 60 ticks
1% fee: every 200 ticks
```

### Optimal Range Width

**Stable Pairs (USDC/USDT)**:

- Narrow: ±0.05% (max efficiency)
- Medium: ±0.1% (balanced)
- Wide: ±0.5% (safety)

**Correlated Pairs (WETH/stETH)**:

- Narrow: ±0.5%
- Medium: ±1%
- Wide: ±2%

**Volatile Pairs (WETH/USDC)**:

- Narrow: ±5%
- Medium: ±10%
- Wide: ±25%

### Capital Efficiency

Concentration factor vs infinite range:

```
Range ±0.1% → 2000x efficient
Range ±1% → 200x efficient
Expand All @@ -81,18 +98,21 @@ Range ±50% → 4x efficient
### Token Calculations

**Below range (token1 only)**:

```
amount1 = L * (sqrtUpper - sqrtLower)
amount0 = 0
```

**Above range (token0 only)**:

```
amount0 = L * (sqrtUpper - sqrtLower) / (sqrtUpper * sqrtLower)
amount1 = 0
```

**In range (both tokens)**:

```
amount0 = L * (sqrtUpper - sqrtCurrent) / (sqrtUpper * sqrtCurrent)
amount1 = L * (sqrtCurrent - sqrtLower)
Expand Down Expand Up @@ -133,5 +153,5 @@ CollectFee(tokenId)
- Tick range validation prevents invalid positions
- Slippage protection on all operations
- Deadline prevents stale transactions
- Non-transferable NFTs prevent position trading
- Only owner can manage their positions
- Position NFTs are non-transferable
- Only owner can manage their positions
Loading