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

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}