Search Apps Documentation Source Content File Folder Download Copy Actions Download

manager.gno

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