Search Apps Documentation Source Content File Folder Download Copy Actions Download

exact_out.gno

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