pool.gno
11.77 Kb · 434 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// Note: Withdrawal fees are applied by the position contract, not here.
200// Only callable by position contract.
201func (i *poolV1) Collect(
202 _ int,
203 rlm realm,
204 token0Path string,
205 token1Path string,
206 fee uint32,
207 recipient address,
208 tickLower int32,
209 tickUpper int32,
210 amount0Requested string,
211 amount1Requested string,
212) (string, string) {
213 if !rlm.IsCurrent() {
214 panic(errors.New(errSpoofedRealm))
215 }
216
217 i.assertPoolUnlocked()
218 halt.AssertIsNotHaltedWithdraw()
219
220 caller := rlm.Previous().Address()
221 access.AssertIsPosition(caller)
222 access.AssertIsValidAddress(recipient)
223
224 i.lockPool(0, rlm)
225 defer i.unlockPool(0, rlm)
226
227 amount0Req := utils.SafeParseInt64(amount0Requested)
228 amount1Req := utils.SafeParseInt64(amount1Requested)
229
230 if amount0Req < 0 || amount1Req < 0 {
231 panic(errors.New(errInvalidInput))
232 }
233
234 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
235 // Generate position key by combining position contract path with tick range
236 // The key is composed of the position contract's address and the tick boundaries,
237 // allowing the pool to uniquely identify and access position data.
238 positionKey := getPositionKey(tickLower, tickUpper)
239 position := mustGetPositionByPool(pool, positionKey)
240
241 amount0 := minRequestedAmount(amount0Req, position.TokensOwed0())
242 amount1 := minRequestedAmount(amount1Req, position.TokensOwed1())
243
244 positionAddr := access.MustGetAddress(prabc.ROLE_POSITION.String())
245
246 if amount0 > 0 {
247 tokenOwed0 := gnsmath.SafeSubInt64(position.TokensOwed0(), amount0)
248 token0Balance := gnsmath.SafeSubInt64(pool.BalanceToken0(), amount0)
249
250 position.SetTokensOwed0(tokenOwed0)
251 pool.SetBalanceToken0(token0Balance)
252 common.SafeGRC20Approve(cross(rlm), pool.Token0Path(), positionAddr, amount0)
253 }
254 if amount1 > 0 {
255 tokenOwed1 := gnsmath.SafeSubInt64(position.TokensOwed1(), amount1)
256 token1Balance := gnsmath.SafeSubInt64(pool.BalanceToken1(), amount1)
257
258 position.SetTokensOwed1(tokenOwed1)
259 pool.SetBalanceToken1(token1Balance)
260 common.SafeGRC20Approve(cross(rlm), pool.Token1Path(), positionAddr, amount1)
261 }
262
263 setPosition(pool, positionKey, *position)
264
265 if err := i.savePool(0, rlm, pool); err != nil {
266 panic(err)
267 }
268
269 return utils.FormatInt(amount0), utils.FormatInt(amount1)
270}
271
272// CollectProtocol collects accumulated protocol fees from swap operations.
273// Only callable by admin or governance.
274// Returns amount0, amount1 representing protocol fees collected.
275func (i *poolV1) CollectProtocol(
276 _ int,
277 rlm realm,
278 token0Path string,
279 token1Path string,
280 fee uint32,
281 recipient address,
282 amount0Requested string, // uint128
283 amount1Requested string, // uint128
284) (string, string) {
285 if !rlm.IsCurrent() {
286 panic(errors.New(errSpoofedRealm))
287 }
288
289 i.assertPoolUnlocked()
290 halt.AssertIsNotHaltedWithdraw()
291
292 previousRealm := rlm.Previous()
293 caller := previousRealm.Address()
294 access.AssertIsAdminOrGovernance(caller)
295
296 common.MustRegistered(token0Path, token1Path)
297
298 i.lockPool(0, rlm)
299 defer i.unlockPool(0, rlm)
300
301 amount0, amount1 := i.collectProtocol(
302 0,
303 rlm,
304 token0Path,
305 token1Path,
306 fee,
307 recipient,
308 amount0Requested,
309 amount1Requested,
310 )
311
312 chain.Emit(
313 "CollectProtocol",
314 "prevAddr", caller.String(),
315 "prevRealm", previousRealm.PkgPath(),
316 "token0Path", token0Path,
317 "token1Path", token1Path,
318 "fee", utils.FormatUint(fee),
319 "recipient", recipient.String(),
320 "internal_amount0", amount0,
321 "internal_amount1", amount1,
322 )
323
324 return amount0, amount1
325}
326
327// collectProtocol performs the actual protocol fee collection.
328// It ensures requested amounts don't exceed available protocol fees.
329// Returns amount0, amount1 as strings representing collected fees.
330func (i *poolV1) collectProtocol(
331 _ int,
332 rlm realm,
333 token0Path string,
334 token1Path string,
335 fee uint32,
336 recipient address,
337 amount0Requested string,
338 amount1Requested string,
339) (string, string) {
340 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
341
342 amount0Req := utils.SafeParseInt64(amount0Requested)
343 amount1Req := utils.SafeParseInt64(amount1Requested)
344
345 if amount0Req < 0 || amount1Req < 0 {
346 panic(errors.New(errInvalidInput))
347 }
348
349 amount0 := minRequestedAmount(amount0Req, pool.ProtocolFeesToken0())
350 amount1 := minRequestedAmount(amount1Req, pool.ProtocolFeesToken1())
351
352 amount0, amount1 = i.saveProtocolFees(pool, amount0, amount1)
353
354 newBalanceToken0, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount0, true)
355 if err != nil {
356 panic(err)
357 }
358 pool.SetBalanceToken0(newBalanceToken0)
359
360 newBalanceToken1, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount1, false)
361 if err != nil {
362 panic(err)
363 }
364 pool.SetBalanceToken1(newBalanceToken1)
365
366 err = i.savePool(0, rlm, pool)
367 if err != nil {
368 panic(err)
369 }
370
371 common.SafeGRC20Transfer(cross(rlm), pool.Token0Path(), recipient, amount0)
372 common.SafeGRC20Transfer(cross(rlm), pool.Token1Path(), recipient, amount1)
373
374 return utils.FormatInt(amount0), utils.FormatInt(amount1)
375}
376
377// saveProtocolFees updates the protocol fee balances after collection.
378// Returns amount0, amount1 representing the fees deducted from protocol reserves.
379func (i *poolV1) saveProtocolFees(pool *pl.Pool, amount0, amount1 int64) (int64, int64) {
380 if pool.ProtocolFeesToken0() < amount0 {
381 panic(errors.New(errUnderflow))
382 }
383 pool.SetProtocolFeesToken0(gnsmath.SafeSubInt64(pool.ProtocolFeesToken0(), amount0))
384
385 if pool.ProtocolFeesToken1() < amount1 {
386 panic(errors.New(errUnderflow))
387 }
388 pool.SetProtocolFeesToken1(gnsmath.SafeSubInt64(pool.ProtocolFeesToken1(), amount1))
389
390 return amount0, amount1
391}
392
393func minRequestedAmount(request, available int64) int64 {
394 if request > available {
395 return available
396 }
397
398 return request
399}
400
401func (i *poolV1) IncreaseObservationCardinalityNext(
402 _ int,
403 rlm realm,
404 token0Path string,
405 token1Path string,
406 fee uint32,
407 cardinalityNext uint16,
408) {
409 if !rlm.IsCurrent() {
410 panic(errors.New(errSpoofedRealm))
411 }
412
413 i.assertPoolUnlocked()
414 halt.AssertIsNotHaltedPool()
415
416 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
417
418 i.lockPool(0, rlm)
419 defer i.unlockPool(0, rlm)
420
421 err := increaseObservationCardinalityNextByPool(pool, cardinalityNext)
422 if err != nil {
423 panic(err)
424 }
425
426 previousRealm := rlm.Previous()
427 chain.Emit(
428 "IncreaseObservationCardinalityNext",
429 "prevAddr", previousRealm.Address().String(),
430 "prevRealm", previousRealm.PkgPath(),
431 "poolPath", pool.PoolPath(),
432 "cardinalityNext", utils.FormatUint(cardinalityNext),
433 )
434}