exact_out.gno
7.55 Kb · 266 lines
1package router
2
3import (
4 "chain"
5 "errors"
6
7 ufmt "gno.land/p/nt/ufmt/v0"
8
9 u256 "gno.land/p/gnoswap/uint256"
10 "gno.land/p/gnoswap/utils"
11
12 "gno.land/r/gnoswap/common"
13 "gno.land/r/gnoswap/emission"
14 "gno.land/r/gnoswap/halt"
15 "gno.land/r/gnoswap/referral"
16)
17
18// ExactOutSwapRoute swaps tokens for an exact output amount.
19//
20// Executes swap to receive exact output tokens.
21// Calculates required input working backwards through route.
22// Useful for buying specific amounts regardless of price.
23//
24// Parameters:
25// - inputToken, outputToken: Token contract paths
26// - amountOut: Exact output amount desired
27// - routeArr: Swap route "TOKEN0:TOKEN1:FEE,TOKEN1:TOKEN2:FEE" (max 7 hops)
28// - quoteArr: Split percentages "70,30" (must sum to 100)
29// - amountInMax: Maximum input to spend (slippage protection)
30// - deadline: Unix timestamp for expiration
31// - referrer: Optional referral address
32//
33// Route calculation:
34// - Works backwards from output to input
35// - Each hop increases required input
36// - Multi-path aggregates total input
37//
38// Returns:
39// - amountIn: Actual input consumed
40// - amountOut: Exact output received
41//
42// Reverts if input > amountInMax or deadline passed.
43func (r *routerV1) ExactOutSwapRoute(
44 _ int,
45 rlm realm,
46 inputToken string,
47 outputToken string,
48 amountOut string,
49 routeArr string,
50 quoteArr string,
51 amountInMax string,
52 deadline int64,
53 referrer string,
54) (string, string) {
55 if !rlm.IsCurrent() {
56 panic(errors.New(errSpoofedRealm))
57 }
58
59 halt.AssertIsNotHaltedRouter()
60
61 common.AssertIsNotHandleNativeCoin()
62
63 assertIsValidRoutePaths(routeArr, inputToken, outputToken)
64 assertIsNotExpired(deadline)
65 assertIsExistsPools(routeArr)
66
67 emission.MintAndDistributeGns(cross(rlm))
68
69 params := SwapRouteParams{
70 inputToken: inputToken,
71 outputToken: outputToken,
72 routeArr: routeArr,
73 quoteArr: quoteArr,
74 deadline: deadline,
75 typ: ExactOut,
76 exactAmount: utils.SafeParseInt64(amountOut),
77 limitAmount: utils.SafeParseInt64(amountInMax),
78 sqrtPriceLimitX96: u256.Zero(), // multi-hop swap is not allowed to set sqrtPriceLimitX96
79 }
80
81 inputAmount, outputAmount := r.exactOutSwapRoute(0, rlm, params, referrer)
82
83 return utils.FormatInt(inputAmount), utils.FormatInt(outputAmount)
84}
85
86// ExactOutSingleSwapRoute swaps tokens for an exact output amount through a single route.
87//
88// Executes single-hop swaps through a single specified route.
89// Allows price limit control via sqrtPriceLimitX96 parameter.
90// Applies slippage protection via maximum input amount.
91//
92// Parameters:
93// - inputToken, outputToken: Token contract paths
94// - amountOut: Exact output amount desired
95// - routeArr: Single swap route (max 3 hops)
96// - amountInMax: Maximum input to spend (slippage protection)
97// - sqrtPriceLimitX96: Price limit for swap execution (0 for no limit)
98// - deadline: Unix timestamp for expiration
99// - referrer: Optional referral address
100//
101// Route format:
102// - Single-hop: "INPUT_TOKEN:OUTPUT_TOKEN:FEE"
103//
104// Returns:
105// - amountIn: Actual input consumed
106// - amountOut: Exact output received
107//
108// Reverts the transaction if the input amount is greater than `amountInMax`,
109// the deadline has passed, or the price limit is exceeded.
110func (r *routerV1) ExactOutSingleSwapRoute(
111 _ int,
112 rlm realm,
113 inputToken string,
114 outputToken string,
115 amountOut string,
116 routeArr string,
117 amountInMax string,
118 sqrtPriceLimitX96 string,
119 deadline int64,
120 referrer string,
121) (string, string) {
122 if !rlm.IsCurrent() {
123 panic(errors.New(errSpoofedRealm))
124 }
125
126 halt.AssertIsNotHaltedRouter()
127
128 common.AssertIsNotHandleNativeCoin()
129
130 assertIsValidSingleSwapRouteArrPath(routeArr, inputToken, outputToken)
131 assertIsValidSqrtPriceLimitX96(sqrtPriceLimitX96)
132 assertIsNotExpired(deadline)
133 assertIsExistsPools(routeArr)
134
135 emission.MintAndDistributeGns(cross(rlm))
136
137 params := SwapRouteParams{
138 inputToken: inputToken,
139 outputToken: outputToken,
140 routeArr: routeArr,
141 quoteArr: "100",
142 deadline: deadline,
143 typ: ExactOut,
144 exactAmount: utils.SafeParseInt64(amountOut),
145 limitAmount: utils.SafeParseInt64(amountInMax),
146 sqrtPriceLimitX96: u256.MustFromDecimal(sqrtPriceLimitX96), // single swap is allowed to set sqrtPriceLimitX96
147 }
148
149 inputAmount, outputAmount := r.exactOutSwapRoute(0, rlm, params, referrer)
150
151 return utils.FormatInt(inputAmount), utils.FormatInt(outputAmount)
152}
153
154// exactOutSwapRoute executes the swap operation and handles token transfers and referral registration.
155//
156// Performs the actual swap operation using commonSwapRoute and handles:
157// - Safe token transfers to the caller
158// - Referral registration and tracking
159// - Event emission for swap completion
160//
161// Parameters:
162// - params: SwapRouteParams containing all swap configuration
163// - referrer: Referral address for registration
164//
165// Returns:
166// - inputAmount: Actual input amount consumed as string
167// - outputAmount: Actual output amount received as string (negative value)
168//
169// Panics if swap execution fails or token transfer fails.
170func (r *routerV1) exactOutSwapRoute(
171 _ int,
172 rlm realm,
173 params SwapRouteParams,
174 referrer string,
175) (int64, int64) {
176 inputAmount, outputAmount, err := r.commonSwapRoute(0, rlm, params)
177 if err != nil {
178 panic(err)
179 }
180
181 previousRealm := rlm.Previous()
182 caller := previousRealm.Address()
183
184 common.SafeGRC20Transfer(cross(rlm), params.outputToken, caller, outputAmount)
185
186 // handle referral registration
187 actualReferrer := referral.TryRegister(cross(rlm), caller, referrer)
188
189 resultInputAmount := inputAmount
190 resultOutputAmount := -outputAmount
191
192 eventAttrs := append([]string{
193 "prevAddr", caller.String(),
194 "prevRealm", previousRealm.PkgPath(),
195 "input", params.inputToken,
196 "output", params.outputToken,
197 "exactAmount", utils.FormatInt(params.exactAmount),
198 "quote", params.quoteArr,
199 "resultInputAmount", utils.FormatInt(resultInputAmount),
200 "resultOutputAmount", utils.FormatInt(resultOutputAmount),
201 "referrer", actualReferrer,
202 }, buildRouteEventAttrs(params.routeArr)...)
203
204 chain.Emit(
205 "ExactOutSwap",
206 eventAttrs...,
207 )
208
209 return resultInputAmount, resultOutputAmount
210}
211
212// ExactOutSwapOperation handles swaps where the output amount is specified.
213type ExactOutSwapOperation struct {
214 router *routerV1
215 baseSwapOperation
216 params ExactOutParams
217}
218
219// NewExactOutSwapOperation creates a new exact-out swap operation.
220func NewExactOutSwapOperation(r *routerV1, pp ExactOutParams) *ExactOutSwapOperation {
221 return &ExactOutSwapOperation{
222 router: r,
223 params: pp,
224 baseSwapOperation: baseSwapOperation{
225 sqrtPriceLimitX96: pp.SqrtPriceLimitX96,
226 },
227 }
228}
229
230// Validate ensures the exact-out swap parameters are valid.
231func (op *ExactOutSwapOperation) Validate() error {
232 amountOut := op.params.AmountOut
233 if amountOut <= 0 {
234 return ufmt.Errorf("invalid amountOut(%d), must be positive", amountOut)
235 }
236
237 // assign a signed reversed `amountOut` to `amountSpecified`
238 // when it's an ExactOut
239 op.amountSpecified = -amountOut
240
241 routes, quotes, err := validateRoutesAndQuotes(op.params.RouteArr, op.params.QuoteArr)
242 if err != nil {
243 return err
244 }
245
246 op.routes = routes
247 op.quotes = quotes
248
249 return nil
250}
251
252// Process executes the exact-out swap operation.
253func (op *ExactOutSwapOperation) Process(_ int, rlm realm) (*SwapResult, error) {
254 resultAmountIn, resultAmountOut, err := op.processRoutes(0, rlm, op.router, ExactOut)
255 if err != nil {
256 return nil, err
257 }
258
259 return &SwapResult{
260 AmountIn: resultAmountIn,
261 AmountOut: resultAmountOut,
262 Routes: op.routes,
263 Quotes: op.quotes,
264 AmountSpecified: op.amountSpecified,
265 }, nil
266}