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}