-
Notifications
You must be signed in to change notification settings - Fork 3
Nialexsan/fix multi swapper #158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3e75cd2
0491dd3
ef24c22
34b33ff
be6b5d5
15901cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -146,26 +146,92 @@ access(all) contract SwapConnectors { | |||||
| access(all) view fun outType(): Type { | ||||||
| return self.outVault | ||||||
| } | ||||||
| /// The estimated amount required to provide a Vault with the desired output balance | ||||||
| /// The estimated amount required to provide a Vault with the desired output balance. | ||||||
| /// | ||||||
| /// Swapper quotes are divided into two groups: | ||||||
| /// 1. Full group: the pool has enough liquidity to fully fulfill forDesired | ||||||
| /// 2. Partial group: the pool can only fulfill part of forDesired | ||||||
| /// | ||||||
| /// Selection policy: | ||||||
| /// - If any swapper is in the full group, return the one with the lowest inAmount | ||||||
| /// - Otherwise, return the partial group quote with the highest outAmount (best liquidity), | ||||||
| /// even if it doesn't have the best rate | ||||||
| access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {DeFiActions.Quote} { | ||||||
| let estimate = self._estimate(amount: forDesired, out: false, reverse: reverse) | ||||||
| var hasFull = false | ||||||
| var bestIdx = 0 | ||||||
| var bestInAmount = UFix64.max | ||||||
| var bestOutAmount = 0.0 | ||||||
|
|
||||||
| for i in InclusiveRange(0, self.swappers.length - 1) { | ||||||
| let quote = (&self.swappers[i] as &{DeFiActions.Swapper}) | ||||||
| .quoteIn(forDesired: forDesired, reverse: reverse) | ||||||
| if quote.inAmount == 0.0 || quote.outAmount == 0.0 { continue } | ||||||
|
|
||||||
| if quote.outAmount >= forDesired { | ||||||
| // full coverage group - comparing between swappers that can full fulfill the forDesire, | ||||||
| // in this case we prefer the quote with minimum inAmount | ||||||
| if !hasFull || quote.inAmount < bestInAmount { | ||||||
| // when hasFull == false, we can skip the second check, because | ||||||
| // there is no bestInAmount yet, this quote itself will be the best temporarily | ||||||
| // when hasFull == true, we compare with the previously best quote, if this | ||||||
| // quote has lower inAmount, then it's better. | ||||||
| hasFull = true | ||||||
|
nialexsan marked this conversation as resolved.
|
||||||
| bestIdx = i | ||||||
| bestInAmount = quote.inAmount | ||||||
| bestOutAmount = quote.outAmount | ||||||
| } | ||||||
| } else if !hasFull { | ||||||
| // partial coverage group (only when no full route found) | ||||||
| // in this case, prefer maximum outAmount | ||||||
| if quote.outAmount > bestOutAmount { | ||||||
| bestIdx = i | ||||||
| bestInAmount = quote.inAmount | ||||||
| bestOutAmount = quote.outAmount | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return MultiSwapperQuote( | ||||||
| inType: reverse ? self.outType() : self.inType(), | ||||||
| outType: reverse ? self.inType() : self.outType(), | ||||||
| inAmount: estimate[1], | ||||||
| outAmount: forDesired, | ||||||
| swapperIndex: Int(estimate[0]) | ||||||
| inAmount: bestInAmount, | ||||||
| outAmount: bestOutAmount, | ||||||
| swapperIndex: bestIdx | ||||||
| ) | ||||||
| } | ||||||
| /// The estimated amount delivered out for a provided input balance | ||||||
| /// The estimated amount delivered out for a provided input balance. | ||||||
| /// | ||||||
| /// Selection policy: among routes where inAmount <= forProvided, prefer the highest outAmount, | ||||||
| /// then the lowest inAmount as a tiebreaker. Routes where inAmount > forProvided are excluded — | ||||||
| /// they cannot be executed with the provided input and would produce misleadingly high outAmounts. | ||||||
| access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DeFiActions.Quote} { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the interface definition:
If reverse=true, we should select the quote with the lowest
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is a disconnect between the interface description and actual implementation we can either change the interface definition, which is cheap,
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this look right for the interface definition?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, that looks correct
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| let estimate = self._estimate(amount: forProvided, out: true, reverse: reverse) | ||||||
| var bestIdx = 0 | ||||||
| var bestInAmount = 0.0 | ||||||
| var bestOutAmount = 0.0 | ||||||
|
|
||||||
| for i in InclusiveRange(0, self.swappers.length - 1) { | ||||||
| let quote = (&self.swappers[i] as &{DeFiActions.Swapper}) | ||||||
| .quoteOut(forProvided: forProvided, reverse: reverse) | ||||||
| if quote.inAmount == 0.0 || quote.outAmount == 0.0 { continue } | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think we also probably want to reject quotes that accept less than we're providing? If we do allow the swapper to change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this change will invalidate partial swapper that provides better quote, like this: in this case using partial swapper 1 is better
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with your example, but I think this point still stands:
Otherwise we can have a scenario like:
(we would randomly pick between swapper 1/2)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that handles all cases. What about:
We'd take quote 1, even though quote 2 is a better price and we only want to trade 10 tokens anyway. I don't think it makes sense to prioritize maximizing In general I don't see a way to make this conceptually symmetrical with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. with optimizing only for ratio, I'd argue that swapping 9 out of 10 inToken for 8.9 outToken is better than swapping 0.1 out of 10 inToken for 0.1 outToken I added inAmount guard, that it should not be more than |
||||||
| if quote.inAmount > forProvided { continue } | ||||||
|
|
||||||
| if quote.outAmount > bestOutAmount { | ||||||
| bestIdx = i | ||||||
| bestInAmount = quote.inAmount | ||||||
| bestOutAmount = quote.outAmount | ||||||
| } else if quote.outAmount == bestOutAmount && quote.inAmount < bestInAmount { | ||||||
| bestIdx = i | ||||||
| bestInAmount = quote.inAmount | ||||||
| bestOutAmount = quote.outAmount | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return MultiSwapperQuote( | ||||||
| inType: reverse ? self.outType() : self.inType(), | ||||||
| outType: reverse ? self.inType() : self.outType(), | ||||||
| inAmount: forProvided, | ||||||
| outAmount: estimate[1], | ||||||
| swapperIndex: Int(estimate[0]) | ||||||
| inAmount: bestInAmount, | ||||||
| outAmount: bestOutAmount, | ||||||
| swapperIndex: bestIdx | ||||||
| ) | ||||||
| } | ||||||
| /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. Implementations may choose | ||||||
|
|
@@ -183,24 +249,6 @@ access(all) contract SwapConnectors { | |||||
| access(all) fun swapBack(quote: {DeFiActions.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} { | ||||||
| return <-self._swap(quote: quote, from: <-residual, reverse: true) | ||||||
| } | ||||||
| /// Returns the the index of the optimal Swapper (result[0]) and the associated amountOut or amountIn (result[0]) | ||||||
| /// as a UFix64 array | ||||||
| access(self) fun _estimate(amount: UFix64, out: Bool, reverse: Bool): [UFix64; 2] { | ||||||
| var res: [UFix64; 2] = out ? [0.0, 0.0] : [0.0, UFix64.max] // maximizing for out, minimizing for in | ||||||
| for i in InclusiveRange(0, self.swappers.length - 1) { | ||||||
| let swapper = &self.swappers[i] as &{DeFiActions.Swapper} | ||||||
| // call the appropriate estimator | ||||||
| let estimate = out | ||||||
| ? swapper.quoteOut(forProvided: amount, reverse: reverse).outAmount | ||||||
| : swapper.quoteIn(forDesired: amount, reverse: reverse).inAmount | ||||||
| // estimates of 0.0 are presumed to be graceful failures - skip them | ||||||
| if estimate > 0.0 && (out ? res[1] < estimate : estimate < res[1]) { | ||||||
| // take minimum for in, maximum for out | ||||||
| res = [UFix64(i), estimate] | ||||||
| } | ||||||
| } | ||||||
| return res | ||||||
| } | ||||||
| /// Swaps the provided Vault in the defined direction. If the quote is not a MultiSwapperQuote, a new quote is | ||||||
| /// requested and the current optimal Swapper used to fulfill the swap. | ||||||
| access(self) fun _swap(quote: {DeFiActions.Quote}?, from: @{FungibleToken.Vault}, reverse: Bool): @{FungibleToken.Vault} { | ||||||
|
|
@@ -607,7 +655,8 @@ access(all) contract SwapConnectors { | |||||
| } | ||||||
|
|
||||||
| // expect output amount as the lesser between the amount available and the maximum amount | ||||||
| var quote = minimumAvail < maxAmount | ||||||
| let usingQuoteOut = minimumAvail < maxAmount | ||||||
| var quote = usingQuoteOut | ||||||
| ? self.swapper.quoteOut(forProvided: self.source.minimumAvailable(), reverse: false) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we take the chance to optimize this function? I noticed when usingQuoteOut is true, This is my optimized version:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add a new issue to apply the optimizations |
||||||
| : self.swapper.quoteIn(forDesired: maxAmount, reverse: false) | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the casting necessary? why not:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cadence assumes that
self.swapperscould be an empty array and it required explicit casting