Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}