Skip to content

Commit a59495f

Browse files
committed
feat(cadence): support exact-out mode quote via swap
1 parent ea5f3a4 commit a59495f

File tree

3 files changed

+167
-27
lines changed

3 files changed

+167
-27
lines changed

cadence/contracts/connectors/SwapConnectors.cdc

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,85 @@ access(all) contract SwapConnectors {
3939
}
4040
}
4141

42+
/// Swap mode metadata for interpreting quote semantics in Swapper.swap().
43+
access(all) enum SwapMode: UInt8 {
44+
access(all) case IN
45+
access(all) case OUT
46+
}
47+
48+
/// A quote that carries swap mode metadata and a receiver capability for exact-output residual input.
49+
access(all) struct ModeQuote : DeFiActions.Quote {
50+
access(all) let inType: Type
51+
access(all) let outType: Type
52+
access(all) let inAmount: UFix64
53+
access(all) let outAmount: UFix64
54+
access(all) let mode: SwapMode
55+
access(all) let leftoverInReceiver: Capability<&{FungibleToken.Receiver}>?
56+
57+
init(
58+
inType: Type,
59+
outType: Type,
60+
inAmount: UFix64,
61+
outAmount: UFix64,
62+
mode: SwapMode,
63+
leftoverInReceiver: Capability<&{FungibleToken.Receiver}>?
64+
) {
65+
self.inType = inType
66+
self.outType = outType
67+
self.inAmount = inAmount
68+
self.outAmount = outAmount
69+
self.mode = mode
70+
self.leftoverInReceiver = leftoverInReceiver
71+
}
72+
}
73+
74+
/// Returns a ModeQuote copy of any DeFiActions.Quote.
75+
access(all) fun asModeQuote(
76+
quote: {DeFiActions.Quote},
77+
mode: SwapMode,
78+
leftoverInReceiver: Capability<&{FungibleToken.Receiver}>?
79+
): ModeQuote {
80+
if let modeQuote = quote as? ModeQuote {
81+
return ModeQuote(
82+
inType: modeQuote.inType,
83+
outType: modeQuote.outType,
84+
inAmount: modeQuote.inAmount,
85+
outAmount: modeQuote.outAmount,
86+
mode: mode,
87+
leftoverInReceiver: leftoverInReceiver ?? modeQuote.leftoverInReceiver
88+
)
89+
}
90+
return ModeQuote(
91+
inType: quote.inType,
92+
outType: quote.outType,
93+
inAmount: quote.inAmount,
94+
outAmount: quote.outAmount,
95+
mode: mode,
96+
leftoverInReceiver: leftoverInReceiver
97+
)
98+
}
99+
100+
/// Returns a ModeQuote configured for exact-input behavior.
101+
access(all) fun asExactInQuote(quote: {DeFiActions.Quote}): ModeQuote {
102+
return self.asModeQuote(
103+
quote: quote,
104+
mode: SwapMode.IN,
105+
leftoverInReceiver: nil
106+
)
107+
}
108+
109+
/// Returns a ModeQuote configured for exact-output behavior.
110+
access(all) fun asExactOutQuote(
111+
quote: {DeFiActions.Quote},
112+
leftoverInReceiver: Capability<&{FungibleToken.Receiver}>
113+
): ModeQuote {
114+
return self.asModeQuote(
115+
quote: quote,
116+
mode: SwapMode.OUT,
117+
leftoverInReceiver: leftoverInReceiver
118+
)
119+
}
120+
42121
/// MultiSwapperQuote
43122
///
44123
/// A MultiSwapper specific DeFiActions.Quote implementation allowing for callers to set the Swapper used in
@@ -174,13 +253,53 @@ access(all) contract SwapConnectors {
174253
/// requested and the optimal Swapper used to fulfill the swap.
175254
/// NOTE: providing a Quote does not guarantee the fulfilled swap will enforce the quote's defined outAmount
176255
access(all) fun swap(quote: {DeFiActions.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
256+
if quote != nil {
257+
if let modeQuote = quote as? ModeQuote {
258+
if modeQuote.mode == SwapMode.OUT {
259+
let residualReceiverCap = modeQuote.leftoverInReceiver
260+
?? panic("Exact-out quote requires a valid leftoverInReceiver capability")
261+
let residualReceiver = residualReceiverCap.borrow()
262+
?? panic("Exact-out quote contains an invalid leftoverInReceiver capability")
263+
264+
let estimate = self.quoteIn(forDesired: modeQuote.outAmount, reverse: false) as! MultiSwapperQuote
265+
let routedQuote = MultiSwapperQuote(
266+
inType: estimate.inType,
267+
outType: estimate.outType,
268+
inAmount: modeQuote.inAmount,
269+
outAmount: modeQuote.outAmount,
270+
swapperIndex: estimate.swapperIndex
271+
)
272+
let optimalSwapper = &self.swappers[routedQuote.swapperIndex] as &{DeFiActions.Swapper}
273+
let vaults <- optimalSwapper.swapExactOut(quote: routedQuote, inVault: <-inVault)
274+
275+
let outVault <- vaults.remove(at: 0)
276+
let leftoverInVault <- vaults.remove(at: 0)
277+
destroy vaults
278+
279+
if leftoverInVault.balance > 0.0 {
280+
residualReceiver.deposit(from: <-leftoverInVault)
281+
} else {
282+
Burner.burn(<-leftoverInVault)
283+
}
284+
285+
return <-outVault
286+
}
287+
}
288+
}
177289
return <-self._swap(quote: quote, from: <-inVault, reverse: false)
178290
}
179291
/// Swap for exact output using the optimal inner Swapper
180292
access(all) fun swapExactOut(quote: {DeFiActions.Quote}, inVault: @{FungibleToken.Vault}): @[{FungibleToken.Vault}] {
181293
var multiQuote = quote as? MultiSwapperQuote
182294
if multiQuote == nil || multiQuote!.swapperIndex >= self.swappers.length {
183-
multiQuote = self.quoteIn(forDesired: quote.outAmount, reverse: false) as! MultiSwapperQuote
295+
let estimate = self.quoteIn(forDesired: quote.outAmount, reverse: false) as! MultiSwapperQuote
296+
multiQuote = MultiSwapperQuote(
297+
inType: estimate.inType,
298+
outType: estimate.outType,
299+
inAmount: quote.inAmount,
300+
outAmount: quote.outAmount,
301+
swapperIndex: estimate.swapperIndex
302+
)
184303
}
185304
let optimalSwapper = &self.swappers[multiQuote!.swapperIndex] as &{DeFiActions.Swapper}
186305
return <- optimalSwapper.swapExactOut(quote: multiQuote!, inVault: <-inVault)

cadence/contracts/connectors/evm/UniswapV3SwapConnectors.cdc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,30 @@ access(all) contract UniswapV3SwapConnectors {
273273

274274
/// Swap exact input -> min output using Uniswap V3 exactInput (default behavior)
275275
access(all) fun swap(quote: {DeFiActions.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
276+
if quote != nil {
277+
if let modeQuote = quote as? SwapConnectors.ModeQuote {
278+
if modeQuote.mode == SwapConnectors.SwapMode.OUT {
279+
let residualReceiverCap = modeQuote.leftoverInReceiver
280+
?? panic("Exact-out quote requires a valid leftoverInReceiver capability")
281+
let residualReceiver = residualReceiverCap.borrow()
282+
?? panic("Exact-out quote contains an invalid leftoverInReceiver capability")
283+
let vaults <- self.swapExactOut(quote: modeQuote, inVault: <-inVault)
284+
285+
let outVault <- vaults.remove(at: 0)
286+
let leftoverInVault <- vaults.remove(at: 0)
287+
destroy vaults
288+
289+
if leftoverInVault.balance > 0.0 {
290+
residualReceiver.deposit(from: <-leftoverInVault)
291+
} else {
292+
Burner.burn(<-leftoverInVault)
293+
}
294+
295+
return <-outVault
296+
}
297+
}
298+
}
299+
276300
let minOut = quote?.outAmount ?? self.quoteOut(forProvided: inVault.balance, reverse: false).outAmount
277301
return <- self._swapExactIn(exactVaultIn: <-inVault, amountOutMin: minOut, reverse: false)
278302
}

cadence/transactions/uniswap-v3-swap-connectors/uniswap_v3_swap_exact_output.cdc

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ transaction(
3434
) {
3535
let swapper: UniswapV3SwapConnectors.Swapper
3636
let tokenInVault: @{FungibleToken.Vault}
37-
let tokenInVaultRef: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
3837
let tokenOutReceiver: &{FungibleToken.Receiver}
3938
let quote: {DeFiActions.Quote}
4039
var tokenOut: UFix64
@@ -66,20 +65,30 @@ transaction(
6665
uniqueID: nil
6766
)
6867

69-
let estimatedQuote = self.swapper.quoteIn(forDesired: desiredAmountOut, reverse: false)
70-
71-
self.quote = SwapConnectors.BasicQuote(
72-
inType: tokenInType,
73-
outType: tokenOutType,
74-
inAmount: maxAmountIn,
75-
outAmount: desiredAmountOut
68+
let tokenInVaultData = MetadataViews.resolveContractViewFromTypeIdentifier(
69+
resourceTypeIdentifier: tokenInType.identifier,
70+
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
71+
) as? FungibleTokenMetadataViews.FTVaultData
72+
?? panic("Could not resolve FTVaultData for ".concat(tokenInType.identifier))
73+
74+
let tokenInReceiverCap = signer.capabilities.storage.issue<&{FungibleToken.Receiver}>(tokenInVaultData.storagePath)
75+
assert(tokenInReceiverCap.check(), message: "Could not issue token-in receiver capability")
76+
77+
self.quote = SwapConnectors.asExactOutQuote(
78+
quote: SwapConnectors.BasicQuote(
79+
inType: tokenInType,
80+
outType: tokenOutType,
81+
inAmount: maxAmountIn,
82+
outAmount: desiredAmountOut
83+
),
84+
leftoverInReceiver: tokenInReceiverCap
7685
)
7786

78-
self.tokenInVaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(
79-
from: /storage/flowTokenVault
87+
let tokenInVaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(
88+
from: tokenInVaultData.storagePath
8089
)!
8190

82-
self.tokenInVault <- self.tokenInVaultRef.withdraw(amount: maxAmountIn)
91+
self.tokenInVault <- tokenInVaultRef.withdraw(amount: maxAmountIn)
8392

8493
// set up output token vault if it doesn't exist
8594
let tokenOutVaultData = MetadataViews.resolveContractViewFromTypeIdentifier(
@@ -109,22 +118,10 @@ transaction(
109118
}
110119

111120
execute {
112-
// swapExactOut returns array: [0] = output vault, [1] = leftover vault
113-
let vaults <- self.swapper.swapExactOut(quote: self.quote, inVault: <-self.tokenInVault)
114-
115-
let tokenOutVault <- vaults.remove(at: 0)
121+
// exact-out is requested through ModeQuote, while keeping swap() as the call entrypoint
122+
let tokenOutVault <- self.swapper.swap(quote: self.quote, inVault: <-self.tokenInVault)
116123
self.tokenOut = tokenOutVault.balance
117-
log("SwapExactOutput: requested \(desiredAmountOut), received \(tokenOutVault.balance)")
118-
119-
let leftoverVault <- vaults.remove(at: 0)
120-
if leftoverVault.balance > 0.0 {
121-
log("Returning leftover: \(leftoverVault.balance)")
122-
self.tokenInVaultRef.deposit(from: <- leftoverVault)
123-
} else {
124-
destroy leftoverVault
125-
}
126-
127-
destroy vaults
124+
log("SwapExactOutput via swap(): requested \(desiredAmountOut), received \(tokenOutVault.balance)")
128125
self.tokenOutReceiver.deposit(from: <-tokenOutVault)
129126
}
130127

0 commit comments

Comments
 (0)