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

manager.gno

11.60 Kb · 383 lines
  1package pool
  2
  3import (
  4	"chain"
  5	"errors"
  6	"time"
  7
  8	prabc "gno.land/p/gnoswap/rbac/v1"
  9	"gno.land/p/gnoswap/utils/v1"
 10	ufmt "gno.land/p/nt/ufmt/v0"
 11
 12	"gno.land/r/gnoswap/access/v1"
 13	"gno.land/r/gnoswap/common"
 14	gns "gno.land/r/gnoswap/gns"
 15	"gno.land/r/gnoswap/halt/v1"
 16	pl "gno.land/r/gnoswap/pool"
 17
 18	en "gno.land/r/gnoswap/emission"
 19)
 20
 21const GNS_TOKEN_KEY string = "gno.land/r/gnoswap/gns.GNS"
 22
 23// CreatePool creates a new concentrated liquidity pool.
 24//
 25// Deploys a new AMM pool for a token pair with the specified fee tier.
 26// Charges the configured pool-creation fee (100 GNS by default).
 27// Accepts either token path order, canonicalizes token0/token1, and sets the
 28// initial price and tick spacing for that canonical order.
 29//
 30// Parameters:
 31//   - _: Noncrossing implementation-call discriminator; pass 0.
 32//   - rlm: Current realm context forwarded unchanged by the pool proxy.
 33//   - token0Path: first token contract path; either order is accepted and the
 34//     stored pair is canonicalized.
 35//   - token1Path: second token contract path; either order is accepted and the
 36//     stored pair is canonicalized.
 37//   - fee: supported pool fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%).
 38//   - sqrtPriceX96: initial square-root price in Q64.96 format, constrained to
 39//     [MIN_SQRT_RATIO, MAX_SQRT_RATIO).
 40//
 41// Tick spacing by fee tier:
 42//   - 0.01%: 1 tick
 43//   - 0.05%: 10 ticks
 44//   - 0.30%: 60 ticks
 45//   - 1.00%: 200 ticks
 46//
 47// Requirements:
 48//   - Tokens must be different after canonicalization/wrapping
 49//   - Fee tier must be supported
 50//   - Pool must not already exist
 51//   - Caller must have the configured creation fee
 52//
 53// Price initialization:
 54//   - The initial sqrt price must be in [MIN_SQRT_RATIO, MAX_SQRT_RATIO).
 55//   - No oracle or external market-price sanity check is performed.
 56//   - If paths are supplied in reverse order, the stored sqrt price is
 57//     inverted to match the canonical token0/token1 order.
 58//
 59// Recovery from an economically inappropriate price:
 60//  1. Add wide-range liquidity at the current price.
 61//  2. Execute a swap to move the price toward the desired market rate.
 62//  3. Remove the liquidity.
 63//
 64// The executor's LP losses offset arbitrage gains, minus fees and slippage.
 65func (i *poolV1) CreatePool(
 66	_ int,
 67	rlm realm,
 68	token0Path string,
 69	token1Path string,
 70	fee uint32,
 71	sqrtPriceX96 string,
 72) {
 73	access.AssertIsRlmCurrent(0, rlm)
 74
 75	i.assertPoolUnlocked()
 76	halt.AssertIsNotHaltedPool()
 77
 78	assertIsSupportedFeeTier(fee)
 79	assertIsNotExistsPoolPath(i, token0Path, token1Path, fee)
 80
 81	i.lockPool(0, rlm)
 82	defer i.unlockPool(0, rlm)
 83
 84	en.MintAndDistributeGns(cross(rlm))
 85
 86	slot0FeeProtocol := i.store.GetSlot0FeeProtocol()
 87
 88	tickSpacing, err := i.GetFeeAmountTickSpacing(fee)
 89	if err != nil {
 90		panic(err)
 91	}
 92
 93	poolInfo := newPoolParams(
 94		token0Path,
 95		token1Path,
 96		fee,
 97		sqrtPriceX96,
 98		tickSpacing,
 99		slot0FeeProtocol,
100	)
101
102	err = poolInfo.update()
103	if err != nil {
104		panic(err)
105	}
106
107	// validate token paths are not the same after wrapping
108	assertIsNotEqualsTokens(poolInfo.token0Path, poolInfo.token1Path)
109
110	// check if wrapped token paths are registered
111	common.MustRegistered(poolInfo.token0Path, poolInfo.token1Path)
112
113	pool := newPool(poolInfo)
114	poolPath := poolInfo.poolPath()
115
116	pools := i.store.GetPools()
117	pools.Set(poolPath, pool)
118
119	err = i.store.SetPools(0, rlm, pools)
120	if err != nil {
121		panic(err)
122	}
123
124	observations := i.store.GetObservations()
125	observations.Set(poolPath, pl.NewPoolObservationsTree(time.Now().Unix()))
126	if err := i.store.SetObservations(0, rlm, observations); err != nil {
127		panic(err)
128	}
129
130	poolCreationFee := i.store.GetPoolCreationFee()
131
132	if poolCreationFee > 0 {
133		previousRealm := rlm.Previous()
134		previousRealmAddr := previousRealm.Address()
135		poolAddr := access.MustGetAddress(prabc.ROLE_POOL.String())
136		gns.TransferFrom(cross(rlm), previousRealmAddr, poolAddr, poolCreationFee)
137
138		chain.Emit(
139			"PoolCreationFee",
140			"prevAddr", previousRealmAddr.String(),
141			"prevRealm", previousRealm.PkgPath(),
142			"poolPath", poolPath,
143			"feeTokenPath", GNS_TOKEN_KEY,
144			"feeAmount", utils.FormatInt(poolCreationFee),
145		)
146	}
147
148	i.settleProtocolFee(0, rlm, GNS_TOKEN_KEY, poolCreationFee)
149
150	previousRealm := rlm.Previous()
151	chain.Emit(
152		"CreatePool",
153		"prevAddr", previousRealm.Address().String(),
154		"prevRealm", previousRealm.PkgPath(),
155		"token0Path", poolInfo.Token0Path(),
156		"token1Path", poolInfo.Token1Path(),
157		"fee", utils.FormatUint(fee),
158		"sqrtPriceX96", poolInfo.SqrtPriceX96().ToString(),
159		"poolPath", poolPath,
160		"tick", utils.FormatInt(pool.Slot0Tick()),
161		"tickSpacing", utils.FormatInt(poolInfo.TickSpacing()),
162	)
163}
164
165// SetFeeProtocol sets the protocol fee denominators for all managed pools.
166//
167// Parameters:
168//   - _: Noncrossing implementation-call discriminator; pass 0.
169//   - rlm: Current realm context forwarded unchanged by the pool proxy.
170//   - feeProtocol0: token0 protocol-fee denominator; must be 0 or 4-10.
171//     Zero disables protocol fees; 4 routes one quarter of swap fees and 10
172//     routes one tenth to the protocol.
173//   - feeProtocol1: token1 protocol-fee denominator with the same 0 or 4-10
174//     constraint and fee share semantics as feeProtocol0.
175//
176// Only callable by admin or governance.
177func (i *poolV1) SetFeeProtocol(_ int, rlm realm, feeProtocol0, feeProtocol1 uint8) {
178	access.AssertIsRlmCurrent(0, rlm)
179
180	i.assertPoolUnlocked()
181	halt.AssertIsNotHaltedPool()
182
183	caller := rlm.Previous().Address()
184	access.AssertIsAdminOrGovernance(caller)
185
186	i.lockPool(0, rlm)
187	defer i.unlockPool(0, rlm)
188
189	err := i.setFeeProtocolInternal(0, rlm, feeProtocol0, feeProtocol1)
190	if err != nil {
191		panic(err)
192	}
193}
194
195// setFeeAmountTickSpacing associates a tick spacing value with a fee amount.
196func (i *poolV1) setFeeAmountTickSpacing(_ int, rlm realm, fee uint32, tickSpacing int32) error {
197	feeAmountTickSpacing := i.store.GetFeeAmountTickSpacing()
198	feeAmountTickSpacing[fee] = tickSpacing
199
200	return i.store.SetFeeAmountTickSpacing(0, rlm, feeAmountTickSpacing)
201}
202
203func (i *poolV1) getPool(poolPath string) (*pl.Pool, error) {
204	pools := i.store.GetPools()
205
206	iPool := pools.Get(poolPath)
207	if iPool == nil {
208		return nil, ufmt.Errorf("expected poolPath(%s) to exist", poolPath)
209	}
210
211	p, ok := iPool.(*pl.Pool)
212	if !ok {
213		return nil, ufmt.Errorf("failed to cast pool to *Pool: %T", iPool)
214	}
215
216	return p, nil
217}
218
219// mustGetPool retrieves a pool instance by its path and ensures it exists.
220func (i *poolV1) mustGetPool(poolPath string) (pool *pl.Pool) {
221	p, err := i.getPool(poolPath)
222	if err != nil {
223		panic(makeErrorWithDetails(errDataNotFound, err.Error()))
224	}
225
226	return p
227}
228
229func (i *poolV1) mustGetPoolBy(token0Path, token1Path string, fee uint32) *pl.Pool {
230	poolPath := GetPoolPath(token0Path, token1Path, fee)
231	return i.mustGetPool(poolPath)
232}
233
234func (i *poolV1) getObservations(poolPath string) (*pl.ObservationTree, error) {
235	observations := i.store.GetObservations()
236	value := observations.Get(poolPath)
237	if value == nil {
238		return nil, ufmt.Errorf("expected observations for poolPath(%s) to exist", poolPath)
239	}
240
241	tree, ok := value.(*pl.ObservationTree)
242	if !ok {
243		return nil, ufmt.Errorf("failed to cast observations for poolPath(%s): %T", poolPath, value)
244	}
245
246	return tree, nil
247}
248
249func (i *poolV1) mustGetObservations(poolPath string) *pl.ObservationTree {
250	tree, err := i.getObservations(poolPath)
251	if err != nil {
252		panic(makeErrorWithDetails(errDataNotFound, err.Error()))
253	}
254	return tree
255}
256
257// savePool updates a pool in the pools map and persists to storage.
258// This ensures that modifications to pool objects are properly saved.
259func (i *poolV1) savePool(_ int, rlm realm, pool *pl.Pool) error {
260	pools := i.store.GetPools()
261	pools.Set(pool.PoolPath(), pool)
262
263	return i.store.SetPools(0, rlm, pools)
264}
265
266// setFeeProtocolInternal updates the protocol fee for all pools and emits an event.
267func (i *poolV1) setFeeProtocolInternal(_ int, rlm realm, feeProtocol0, feeProtocol1 uint8) error {
268	oldFee := i.store.GetSlot0FeeProtocol()
269	newFee, err := i.setFeeProtocol(0, rlm, feeProtocol0, feeProtocol1)
270	if err != nil {
271		return err
272	}
273
274	feeProtocol0Old := oldFee % 16
275	feeProtocol1Old := oldFee >> 4
276
277	previousRealm := rlm.Previous()
278	chain.Emit(
279		"SetFeeProtocol",
280		"prevAddr", previousRealm.Address().String(),
281		"prevRealm", previousRealm.PkgPath(),
282		"prevFeeProtocol0", utils.FormatUint(feeProtocol0Old),
283		"prevFeeProtocol1", utils.FormatUint(feeProtocol1Old),
284		"feeProtocol0", utils.FormatUint(feeProtocol0),
285		"feeProtocol1", utils.FormatUint(feeProtocol1),
286		"newFee", utils.FormatUint(newFee),
287	)
288
289	return nil
290}
291
292// setFeeProtocol updates the protocol fee configuration for all managed pools.
293//
294// This function combines the protocol fee values for token0 and token1 into a single `uint8` value,
295// where:
296//   - Lower 4 bits store feeProtocol0 (for token0).
297//   - Upper 4 bits store feeProtocol1 (for token1).
298//
299// The updated fee protocol is applied uniformly to all pools managed by the system.
300//
301// Parameters:
302//   - feeProtocol0: protocol fee for token0 (must be 0 or between 4 and 10 inclusive).
303//   - feeProtocol1: protocol fee for token1 (must be 0 or between 4 and 10 inclusive).
304//
305// Returns:
306//   - newFee (uint8): the combined fee protocol value.
307//
308// Example:
309// If feeProtocol0 = 4 and feeProtocol1 = 5:
310//
311//	newFee = 4 + (5 << 4)
312//	// Results in: 0x54 (84 in decimal)
313//	// Binary: 0101 0100
314//	//         ^^^^ ^^^^
315//	//       fee1=5  fee0=4
316//
317// Notes:
318//   - This function ensures that all pools under management are updated to use the same fee protocol.
319//   - Caller restrictions (e.g., admin or governance) are not enforced in this function.
320//   - Ensure the system is not halted before updating fees.
321func (i *poolV1) setFeeProtocol(_ int, rlm realm, feeProtocol0, feeProtocol1 uint8) (uint8, error) {
322	if err := validateFeeProtocol(feeProtocol0, feeProtocol1); err != nil {
323		return 0, err
324	}
325
326	// combine both protocol fee into a single byte:
327	// - feePrtocol0 occupies the lower 4 bits
328	// - feeProtocol1 is shifted the lower 4 positions to occupy the upper 4 bits
329	newFee := feeProtocol0 + (feeProtocol1 << 4) // ( << 4 ) = ( * 16 )
330
331	pools := i.store.GetPools()
332
333	// Update slot0 for each pool
334	pools.Iterate("", "", func(poolPath string, poolI any) bool {
335		pool, ok := poolI.(*pl.Pool)
336		if !ok {
337			panic(ufmt.Errorf("failed to cast pool to *Pool: %T", pool))
338		}
339
340		slot0 := pool.Slot0()
341		slot0.SetFeeProtocol(newFee)
342
343		pool.SetSlot0(slot0)
344		return false
345	})
346
347	// update slot0
348	err := i.store.SetSlot0FeeProtocol(0, rlm, newFee)
349	if err != nil {
350		return 0, err
351	}
352
353	return newFee, nil
354}
355
356// validateFeeProtocol validates the fee protocol values for token0 and token1.
357//
358// This function checks whether the provided fee protocol values (`feeProtocol0` and `feeProtocol1`)
359// are valid using the `isValidFeeProtocolValue` function. If either value is invalid, it returns
360// an error indicating that the protocol fee percentage is invalid.
361//
362// Parameters:
363//   - feeProtocol0: uint8, the fee protocol value for token0.
364//   - feeProtocol1: uint8, the fee protocol value for token1.
365//
366// Returns:
367//   - error: Returns `errInvalidProtocolFeePct` if either `feeProtocol0` or `feeProtocol1` is invalid.
368//     Returns `nil` if both values are valid.
369func validateFeeProtocol(feeProtocol0, feeProtocol1 uint8) error {
370	if !isValidFeeProtocolValue(feeProtocol0) || !isValidFeeProtocolValue(feeProtocol1) {
371		return errors.New(errInvalidProtocolFeePct)
372	}
373	return nil
374}
375
376// isValidFeeProtocolValue checks if a fee protocol value is within acceptable range.
377// Valid values are either 0 (disabled) or between 4 and 10 inclusive.
378//
379// The value is used as a denominator: protocolFee = swapFee / feeProtocol
380// (e.g., feeProtocol=4 means 1/4 = 25% of swap fees go to protocol)
381func isValidFeeProtocolValue(value uint8) bool {
382	return value == 0 || (value >= 4 && value <= 10)
383}