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

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