Search Apps Documentation Source Content File Folder Download Copy Actions Download

pool.gno

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