Search Apps Documentation Source Content File Folder Download Copy Actions Download

manager.gno

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