Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}