base.gno
9.00 Kb ยท 330 lines
1package v1
2
3import (
4 "chain/banker"
5 "chain/runtime"
6 "errors"
7 "strings"
8
9 prbac "gno.land/p/gnoswap/rbac"
10 u256 "gno.land/p/gnoswap/uint256"
11 "gno.land/p/nt/ufmt"
12
13 "gno.land/r/gnoland/wugnot"
14 "gno.land/r/gnoswap/access"
15 "gno.land/r/gnoswap/common"
16)
17
18const (
19 SINGLE_HOP_ROUTE int = 1
20
21 INITIAL_WUGNOT_BALANCE int64 = 0
22)
23
24// swap can be done by multiple pools
25// to separate each pool, we use POOL_SEPARATOR
26const (
27 POOL_SEPARATOR = "*POOL*"
28 wugnotPath = "gno.land/r/gnoland/wugnot"
29)
30
31type RouterOperation interface {
32 Validate() error
33 Process() (*SwapResult, error)
34}
35
36// executeSwapOperation validates and processes a swap operation.
37func executeSwapOperation(op RouterOperation) (*SwapResult, error) {
38 if err := op.Validate(); err != nil {
39 return nil, err
40 }
41
42 result, err := op.Process()
43 if err != nil {
44 return nil, err
45 }
46
47 return result, nil
48}
49
50type BaseSwapParams struct {
51 InputToken string
52 OutputToken string
53 RouteArr string
54 QuoteArr string
55 SqrtPriceLimitX96 *u256.Uint
56 Deadline int64
57}
58
59// common swap operation
60type baseSwapOperation struct {
61 sqrtPriceLimitX96 *u256.Uint
62 withUnwrap bool
63 userBeforeWugnotBalance int64
64 userWrappedWugnot int64
65 routes []string
66 quotes []string
67 amountSpecified int64
68}
69
70// handleNativeTokenWrapping handles wrapping and unwrapping of native tokens.
71func (op *baseSwapOperation) handleNativeTokenWrapping(
72 inputToken string,
73 outputToken string,
74 specifiedAmount int64,
75) error {
76 isInputNativeToken := common.IsGNOTNativePath(inputToken)
77 isOutputNativeToken := common.IsGNOTNativePath(outputToken)
78
79 // no native token
80 if !isInputNativeToken && !isOutputNativeToken {
81 return nil
82 }
83
84 // save current user's WGNOT amount
85 op.userBeforeWugnotBalance = wugnot.BalanceOf(runtime.PreviousRealm().Address())
86
87 // Handle input token independently
88 if isInputNativeToken {
89 sent := banker.OriginSend()
90 ugnotSentByUser := sent.AmountOf(GNOT_DENOM)
91
92 if ugnotSentByUser != specifiedAmount {
93 return ufmt.Errorf("ugnot sent by user(%d) is not equal to amountSpecified(%d)", ugnotSentByUser, specifiedAmount)
94 }
95
96 // wrap user's GNOT to WUGNOT
97 if ugnotSentByUser > 0 {
98 if err := wrapWithTransfer(runtime.PreviousRealm().Address(), ugnotSentByUser); err != nil {
99 return err
100 }
101 }
102
103 op.userWrappedWugnot = ugnotSentByUser
104 }
105
106 // Handle output token independently
107 if isOutputNativeToken {
108 op.withUnwrap = true
109 }
110
111 return nil
112}
113
114// processRoutes processes all swap routes and returns total amounts.
115func (op *baseSwapOperation) processRoutes(r *routerV1, swapType SwapType) (int64, int64, error) {
116 resultAmountIn, resultAmountOut := int64(0), int64(0)
117 remainRequestAmount := op.amountSpecified
118
119 for i, route := range op.routes {
120 toSwapAmount := int64(0)
121
122 // if it's the last route, use the remaining amount
123 isLastRoute := i == len(op.routes)-1
124 if !isLastRoute {
125 // calculate the amount to swap for this route
126 swapAmount, err := calculateSwapAmountByQuote(op.amountSpecified, op.quotes[i])
127 if err != nil {
128 return 0, 0, err
129 }
130
131 // update the remaining amount
132 remainRequestAmount = safeSubInt64(remainRequestAmount, swapAmount)
133 toSwapAmount = swapAmount
134 } else {
135 toSwapAmount = remainRequestAmount
136 }
137
138 amountIn, amountOut, err := op.processRoute(r, route, toSwapAmount, swapType)
139 if err != nil {
140 return 0, 0, err
141 }
142
143 resultAmountIn = safeAddInt64(resultAmountIn, amountIn)
144 resultAmountOut = safeAddInt64(resultAmountOut, amountOut)
145 }
146
147 return resultAmountIn, resultAmountOut, nil
148}
149
150// processRoute processes a single route with specified swap amount.
151func (op *baseSwapOperation) processRoute(
152 r *routerV1,
153 route string,
154 toSwap int64,
155 swapType SwapType,
156) (amountIn, amountOut int64, err error) {
157 numHops := strings.Count(route, POOL_SEPARATOR) + 1
158 assertHopsInRange(numHops)
159
160 switch numHops {
161 case SINGLE_HOP_ROUTE:
162 amountIn, amountOut = r.handleSingleSwap(route, toSwap, op.withUnwrap, op.sqrtPriceLimitX96)
163 default:
164 amountIn, amountOut = r.handleMultiSwap(swapType, route, numHops, toSwap, op.withUnwrap)
165 }
166
167 return amountIn, amountOut, nil
168}
169
170// handleSingleSwap executes a single-hop swap with the specified amount.
171func (r *routerV1) handleSingleSwap(route string, amountSpecified int64, withUnwrap bool, sqrtPriceLimitX96 *u256.Uint) (int64, int64) {
172 input, output, fee := getDataForSinglePath(route)
173 singleParams := SingleSwapParams{
174 tokenIn: input,
175 tokenOut: output,
176 fee: fee,
177 amountSpecified: amountSpecified,
178 withUnwrap: withUnwrap,
179 sqrtPriceLimitX96: sqrtPriceLimitX96,
180 }
181
182 return r.singleSwap(&singleParams)
183}
184
185// handleMultiSwap processes multi-hop swaps across multiple pools.
186func (r *routerV1) handleMultiSwap(
187 swapType SwapType,
188 route string,
189 numHops int,
190 amountSpecified int64,
191 withUnwrap bool,
192) (int64, int64) {
193 recipient := access.MustGetAddress(prbac.ROLE_ROUTER.String())
194
195 switch swapType {
196 case ExactIn:
197 input, output, fee := getDataForMultiPath(route, 0) // first data
198 sp := newSwapParams(input, output, fee, recipient, amountSpecified, withUnwrap)
199 return r.multiSwap(*sp, numHops, route)
200 case ExactOut:
201 input, output, fee := getDataForMultiPath(route, numHops-1) // last data
202 sp := newSwapParams(input, output, fee, recipient, amountSpecified, withUnwrap)
203 return r.multiSwapNegative(*sp, numHops, route)
204 default:
205 panic(errInvalidSwapType)
206 }
207}
208
209// SwapRouteParams contains all parameters needed for swap route execution
210type SwapRouteParams struct {
211 inputToken string
212 outputToken string
213 routeArr string
214 quoteArr string
215 deadline int64
216 typ SwapType
217 exactAmount int64 // amountIn for ExactIn, amountOut for ExactOut
218 limitAmount int64 // amountOutMin for ExactIn, amountInMax for ExactOut
219 sqrtPriceLimitX96 *u256.Uint // if sqrtPriceLimitX96 is zero string, it will be set to MIN_PRICE or MAX_PRICE
220}
221
222func (p *SwapRouteParams) ExactAmount() int64 {
223 return p.exactAmount
224}
225
226// when exact out, calculate amount to fetch from pool including router fee
227func (p *SwapRouteParams) ExpectedExactAmountByFee(feeBps uint64) int64 {
228 if p.typ == ExactIn {
229 return p.exactAmount
230 }
231
232 return calculateExactOutWithRouterFee(p.exactAmount, feeBps)
233}
234
235// IsUnwrap checks if the swap output is native token.
236func (p *SwapRouteParams) IsUnwrap() bool {
237 return p.outputToken == GNOT_DENOM
238}
239
240func (p *SwapRouteParams) SwapCount() int64 {
241 swapCount := int64(0)
242
243 for _, route := range strings.Split(p.routeArr, ",") {
244 swapCount += int64(strings.Count(route, POOL_SEPARATOR) + 1)
245 }
246
247 return swapCount
248}
249
250func (p *SwapRouteParams) IsSetSqrtPriceLimitX96() bool {
251 return p.sqrtPriceLimitX96 != nil && !p.sqrtPriceLimitX96.IsZero()
252}
253
254// createSwapOperation creates the appropriate swap operation based on swap type.
255func createSwapOperation(r *routerV1, params SwapRouteParams) (RouterOperation, error) {
256 baseParams := BaseSwapParams{
257 InputToken: params.inputToken,
258 OutputToken: params.outputToken,
259 RouteArr: params.routeArr,
260 QuoteArr: params.quoteArr,
261 SqrtPriceLimitX96: params.sqrtPriceLimitX96,
262 Deadline: params.deadline,
263 }
264
265 switch params.typ {
266 case ExactIn:
267 pp := NewExactInParams(baseParams, params.ExactAmount(), params.limitAmount)
268 return NewExactInSwapOperation(r, pp), nil
269 case ExactOut:
270 routerFee := r.store.GetSwapFee()
271 pp := NewExactOutParams(baseParams, params.ExpectedExactAmountByFee(routerFee), params.limitAmount)
272 return NewExactOutSwapOperation(r, pp), nil
273 default:
274 msg := addDetailToError(errInvalidSwapType, "unknown swap type")
275 return nil, errors.New(msg)
276 }
277}
278
279// extractSwapOperationData extracts common data from swap operation.
280func extractSwapOperationData(op RouterOperation) (int64, int64, error) {
281 var baseOp *baseSwapOperation
282 switch typedOp := op.(type) {
283 case *ExactInSwapOperation:
284 baseOp = &typedOp.baseSwapOperation
285 case *ExactOutSwapOperation:
286 baseOp = &typedOp.baseSwapOperation
287 default:
288 return 0, 0, ufmt.Errorf("unexpected operation type: %T", op)
289 }
290
291 return baseOp.userBeforeWugnotBalance, baseOp.userWrappedWugnot, nil
292}
293
294// commonSwapRoute handles the common logic for both ExactIn and ExactOut swaps.
295func (r *routerV1) commonSwapRoute(params SwapRouteParams) (int64, int64, error) {
296 op, err := createSwapOperation(r, params)
297 if err != nil {
298 return 0, 0, err
299 }
300
301 result, err := executeSwapOperation(op)
302 if err != nil {
303 msg := addDetailToError(
304 errInvalidInput,
305 ufmt.Sprintf("invalid %s SwapOperation: %s", params.typ.String(), err.Error()),
306 )
307 return 0, 0, errors.New(msg)
308 }
309
310 userBeforeWugnotBalance, userWrappedWugnot, err := extractSwapOperationData(op)
311 if err != nil {
312 return 0, 0, err
313 }
314
315 inputAmount, outputAmount := r.finalizeSwap(
316 params.inputToken,
317 params.outputToken,
318 result.AmountIn,
319 result.AmountOut,
320 params.typ,
321 params.limitAmount,
322 userBeforeWugnotBalance,
323 userWrappedWugnot,
324 params.ExactAmount(),
325 params.SwapCount(),
326 params.IsSetSqrtPriceLimitX96(),
327 )
328
329 return inputAmount, outputAmount, nil
330}