pool.gno
11.40 Kb · 431 lines
1package v1
2
3import (
4 "chain"
5
6 "gno.land/r/gnoswap/common"
7 "gno.land/r/gnoswap/halt"
8 pl "gno.land/r/gnoswap/pool"
9
10 i256 "gno.land/p/gnoswap/int256"
11 u256 "gno.land/p/gnoswap/uint256"
12
13 prabc "gno.land/p/gnoswap/rbac"
14 _ "gno.land/r/gnoswap/rbac"
15
16 "gno.land/r/gnoswap/access"
17)
18
19// Mint adds liquidity to a pool position.
20//
21// Increases liquidity for a position within specified tick range.
22// Calculates required token amounts based on current pool price.
23// Updates tick state and transfers tokens atomically.
24//
25// Parameters:
26// - token0Path, token1Path: Token contract paths
27// - fee: Fee tier (100, 500, 3000, 10000 = 0.01%, 0.05%, 0.3%, 1%)
28// - tickLower, tickUpper: Price range boundaries (must be tick-aligned)
29// - liquidityAmount: Liquidity to add (decimal string)
30// - positionCaller: Address that provides tokens for the mint operation
31//
32// Returns:
33// - amount0: Token0 amount consumed (decimal string)
34// - amount1: Token1 amount consumed (decimal string)
35//
36// Requirements:
37// - Pool must exist for token pair and fee
38// - Liquidity amount must be positive
39// - Ticks must be valid and aligned to spacing
40//
41// Only callable by position contract.
42func (i *poolV1) Mint(
43 _ int,
44 rlm realm,
45 token0Path string,
46 token1Path string,
47 fee uint32,
48 tickLower int32,
49 tickUpper int32,
50 liquidityAmount string,
51 positionCaller address,
52) (string, string) {
53 if !rlm.IsCurrent() {
54 panic(errSpoofedRealm)
55 }
56
57 i.assertPoolUnlocked()
58 halt.AssertIsNotHaltedPool()
59
60 caller := rlm.Previous().Address()
61 access.AssertIsPosition(caller)
62 access.AssertIsValidAddress(positionCaller)
63
64 i.lockPool(0, rlm)
65 defer i.unlockPool(0, rlm)
66
67 liquidity := u256.MustFromDecimal(liquidityAmount)
68 if liquidity.IsZero() {
69 panic(errZeroLiquidity)
70 }
71
72 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
73
74 tickSpacing := pool.TickSpacing()
75 checkTickSpacing(tickLower, tickSpacing)
76 checkTickSpacing(tickUpper, tickSpacing)
77
78 liquidityDelta := safeConvertToInt128(liquidity)
79 positionParam := newModifyPositionParams(positionCaller, tickLower, tickUpper, liquidityDelta)
80 _, amount0, amount1, err := modifyPosition(pool, positionParam)
81 if err != nil {
82 panic(err)
83 }
84
85 poolAddr := access.MustGetAddress(prabc.ROLE_POOL.String())
86
87 if amount0.Gt(zero) {
88 i.safeTransferFrom(0, rlm, pool, positionCaller, poolAddr, pool.Token0Path(), amount0, true)
89 }
90
91 if amount1.Gt(zero) {
92 i.safeTransferFrom(0, rlm, pool, positionCaller, poolAddr, pool.Token1Path(), amount1, false)
93 }
94
95 // Save pool state after modifyPosition may have updated liquidity
96 err = i.savePool(0, rlm, pool)
97 if err != nil {
98 panic(err)
99 }
100
101 return amount0.ToString(), amount1.ToString()
102}
103
104// Burn removes liquidity from a position.
105//
106// Decreases liquidity and calculates tokens owed to position owner.
107// Updates tick state but doesn't transfer tokens (use Collect).
108// Two-step process prevents reentrancy attacks.
109//
110// Parameters:
111// - token0Path, token1Path: Token contract paths
112// - fee: Fee tier matching the pool
113// - tickLower, tickUpper: Position's price range
114// - liquidityAmount: Liquidity to remove (uint128)
115// - positionCaller: Position owner for validation
116//
117// Returns:
118// - amount0: Token0 owed to position (decimal string)
119// - amount1: Token1 owed to position (decimal string)
120//
121// Note: Tokens remain in pool until Collect is called.
122// Only callable by position contract.
123func (i *poolV1) Burn(
124 _ int,
125 rlm realm,
126 token0Path string,
127 token1Path string,
128 fee uint32,
129 tickLower int32,
130 tickUpper int32,
131 liquidityAmount string, // uint128
132 positionCaller address,
133) (string, string) {
134 if !rlm.IsCurrent() {
135 panic(errSpoofedRealm)
136 }
137
138 i.assertPoolUnlocked()
139 halt.AssertIsNotHaltedWithdraw()
140
141 caller := rlm.Previous().Address()
142 access.AssertIsPosition(caller)
143 access.AssertIsValidAddress(positionCaller)
144
145 i.lockPool(0, rlm)
146 defer i.unlockPool(0, rlm)
147
148 liqAmount := u256.MustFromDecimal(liquidityAmount)
149 liqAmountInt256 := safeConvertToInt128(liqAmount)
150 liqDelta := i256.Zero().Neg(liqAmountInt256)
151
152 posParams := newModifyPositionParams(positionCaller, tickLower, tickUpper, liqDelta)
153 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
154 position, amount0, amount1, err := modifyPosition(pool, posParams)
155 if err != nil {
156 panic(err)
157 }
158
159 if amount0.Gt(zero) || amount1.Gt(zero) {
160 amount0 = toUint128(amount0)
161 amount1 = toUint128(amount1)
162
163 position.SetTokensOwed0(safeAddInt64(position.TokensOwed0(), safeConvertToInt64(amount0)))
164 position.SetTokensOwed1(safeAddInt64(position.TokensOwed1(), safeConvertToInt64(amount1)))
165 }
166
167 positionKey := getPositionKey(tickLower, tickUpper)
168
169 setPosition(pool, positionKey, position)
170
171 err = i.savePool(0, rlm, pool)
172 if err != nil {
173 panic(err)
174 }
175
176 // actual token transfer happens in Collect()
177 return amount0.ToString(), amount1.ToString()
178}
179
180// Collect transfers owed tokens from a position to recipient.
181//
182// Claims tokens from burned liquidity and accumulated fees.
183// Supports partial collection via amount limits.
184//
185// Parameters:
186// - token0Path, token1Path: Token contract paths
187// - fee: Fee tier of the pool
188// - recipient: Address to receive tokens
189// - tickLower, tickUpper: Position's price range
190// - amount0Requested, amount1Requested: Max amounts to collect (use MAX_UINT128 for all)
191//
192// Returns:
193// - amount0: Token0 amount transferred (before any withdrawal fees)
194// - amount1: Token1 amount transferred (before any withdrawal fees)
195//
196// Note: Withdrawal fees are applied by the position contract, not here.
197// Only callable by position contract.
198func (i *poolV1) Collect(
199 _ int,
200 rlm realm,
201 token0Path string,
202 token1Path string,
203 fee uint32,
204 recipient address,
205 tickLower int32,
206 tickUpper int32,
207 amount0Requested string,
208 amount1Requested string,
209) (string, string) {
210 if !rlm.IsCurrent() {
211 panic(errSpoofedRealm)
212 }
213
214 i.assertPoolUnlocked()
215 halt.AssertIsNotHaltedWithdraw()
216
217 caller := rlm.Previous().Address()
218 access.AssertIsPosition(caller)
219 access.AssertIsValidAddress(recipient)
220
221 i.lockPool(0, rlm)
222 defer i.unlockPool(0, rlm)
223
224 amount0Req := safeParseInt64(amount0Requested)
225 amount1Req := safeParseInt64(amount1Requested)
226
227 if amount0Req < 0 || amount1Req < 0 {
228 panic(errInvalidInput)
229 }
230
231 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
232 // Generate position key by combining position contract path with tick range
233 // The key is composed of the position contract's address and the tick boundaries,
234 // allowing the pool to uniquely identify and access position data.
235 positionKey := getPositionKey(tickLower, tickUpper)
236 position := mustGetPositionByPool(pool, positionKey)
237
238 amount0 := minRequestedAmount(amount0Req, position.TokensOwed0())
239 amount1 := minRequestedAmount(amount1Req, position.TokensOwed1())
240
241 positionAddr := access.MustGetAddress(prabc.ROLE_POSITION.String())
242
243 if amount0 > 0 {
244 tokenOwed0 := safeSubInt64(position.TokensOwed0(), amount0)
245 token0Balance := safeSubInt64(pool.BalanceToken0(), amount0)
246
247 position.SetTokensOwed0(tokenOwed0)
248 pool.SetBalanceToken0(token0Balance)
249 common.SafeGRC20Approve(cross(rlm), pool.Token0Path(), positionAddr, amount0)
250 }
251 if amount1 > 0 {
252 tokenOwed1 := safeSubInt64(position.TokensOwed1(), amount1)
253 token1Balance := safeSubInt64(pool.BalanceToken1(), amount1)
254
255 position.SetTokensOwed1(tokenOwed1)
256 pool.SetBalanceToken1(token1Balance)
257 common.SafeGRC20Approve(cross(rlm), pool.Token1Path(), positionAddr, amount1)
258 }
259
260 setPosition(pool, positionKey, *position)
261
262 if err := i.savePool(0, rlm, pool); err != nil {
263 panic(err)
264 }
265
266 return formatInt(amount0), formatInt(amount1)
267}
268
269// CollectProtocol collects accumulated protocol fees from swap operations.
270// Only callable by admin or governance.
271// Returns amount0, amount1 representing protocol fees collected.
272func (i *poolV1) CollectProtocol(
273 _ int,
274 rlm realm,
275 token0Path string,
276 token1Path string,
277 fee uint32,
278 recipient address,
279 amount0Requested string, // uint128
280 amount1Requested string, // uint128
281) (string, string) {
282 if !rlm.IsCurrent() {
283 panic(errSpoofedRealm)
284 }
285
286 i.assertPoolUnlocked()
287 halt.AssertIsNotHaltedWithdraw()
288
289 previousRealm := rlm.Previous()
290 caller := previousRealm.Address()
291 access.AssertIsAdminOrGovernance(caller)
292
293 common.MustRegistered(token0Path, token1Path)
294
295 i.lockPool(0, rlm)
296 defer i.unlockPool(0, rlm)
297
298 amount0, amount1 := i.collectProtocol(
299 0,
300 rlm,
301 token0Path,
302 token1Path,
303 fee,
304 recipient,
305 amount0Requested,
306 amount1Requested,
307 )
308
309 chain.Emit(
310 "CollectProtocol",
311 "prevAddr", caller.String(),
312 "prevRealm", previousRealm.PkgPath(),
313 "token0Path", token0Path,
314 "token1Path", token1Path,
315 "fee", formatUint(fee),
316 "recipient", recipient.String(),
317 "internal_amount0", amount0,
318 "internal_amount1", amount1,
319 )
320
321 return amount0, amount1
322}
323
324// collectProtocol performs the actual protocol fee collection.
325// It ensures requested amounts don't exceed available protocol fees.
326// Returns amount0, amount1 as strings representing collected fees.
327func (i *poolV1) collectProtocol(
328 _ int,
329 rlm realm,
330 token0Path string,
331 token1Path string,
332 fee uint32,
333 recipient address,
334 amount0Requested string,
335 amount1Requested string,
336) (string, string) {
337 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
338
339 amount0Req := safeParseInt64(amount0Requested)
340 amount1Req := safeParseInt64(amount1Requested)
341
342 if amount0Req < 0 || amount1Req < 0 {
343 panic(errInvalidInput)
344 }
345
346 amount0 := minRequestedAmount(amount0Req, pool.ProtocolFeesToken0())
347 amount1 := minRequestedAmount(amount1Req, pool.ProtocolFeesToken1())
348
349 amount0, amount1 = i.saveProtocolFees(pool, amount0, amount1)
350
351 newBalanceToken0, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount0, true)
352 if err != nil {
353 panic(err)
354 }
355 pool.SetBalanceToken0(newBalanceToken0)
356
357 newBalanceToken1, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount1, false)
358 if err != nil {
359 panic(err)
360 }
361 pool.SetBalanceToken1(newBalanceToken1)
362
363 err = i.savePool(0, rlm, pool)
364 if err != nil {
365 panic(err)
366 }
367
368 common.SafeGRC20Transfer(cross(rlm), pool.Token0Path(), recipient, amount0)
369 common.SafeGRC20Transfer(cross(rlm), pool.Token1Path(), recipient, amount1)
370
371 return formatInt(amount0), formatInt(amount1)
372}
373
374// saveProtocolFees updates the protocol fee balances after collection.
375// Returns amount0, amount1 representing the fees deducted from protocol reserves.
376func (i *poolV1) saveProtocolFees(pool *pl.Pool, amount0, amount1 int64) (int64, int64) {
377 if pool.ProtocolFeesToken0() < amount0 {
378 panic(errUnderflow)
379 }
380 pool.SetProtocolFeesToken0(safeSubInt64(pool.ProtocolFeesToken0(), amount0))
381
382 if pool.ProtocolFeesToken1() < amount1 {
383 panic(errUnderflow)
384 }
385 pool.SetProtocolFeesToken1(safeSubInt64(pool.ProtocolFeesToken1(), amount1))
386
387 return amount0, amount1
388}
389
390func minRequestedAmount(request, available int64) int64 {
391 if request > available {
392 return available
393 }
394
395 return request
396}
397
398func (i *poolV1) IncreaseObservationCardinalityNext(
399 _ int,
400 rlm realm,
401 token0Path string,
402 token1Path string,
403 fee uint32,
404 cardinalityNext uint16,
405) {
406 if !rlm.IsCurrent() {
407 panic(errSpoofedRealm)
408 }
409
410 i.assertPoolUnlocked()
411 halt.AssertIsNotHaltedPool()
412
413 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
414
415 i.lockPool(0, rlm)
416 defer i.unlockPool(0, rlm)
417
418 err := increaseObservationCardinalityNextByPool(pool, cardinalityNext)
419 if err != nil {
420 panic(err)
421 }
422
423 previousRealm := rlm.Previous()
424 chain.Emit(
425 "IncreaseObservationCardinalityNext",
426 "prevAddr", previousRealm.Address().String(),
427 "prevRealm", previousRealm.PkgPath(),
428 "poolPath", pool.PoolPath(),
429 "cardinalityNext", formatUint(cardinalityNext),
430 )
431}