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