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}