Smart Contract
UniswapV2SwapConnectors
A.0e5b1dececaca3a8.UniswapV2SwapConnectors
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import Burner from 0xf233dcee88fe0abe
4import EVM from 0xe467b9dd11fa00df
5import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
6import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
7import FlowEVMBridge from 0x1e4aa0b87d10b141
8
9import DeFiActions from 0x92195d814edf9cb0
10import SwapConnectors from 0x0bce04a00aedf132
11
12/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
13/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT
14/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
15///
16/// UniswapV2SwapConnectors
17///
18/// DeFiActions Swapper connector implementation fitting UniswapV2 EVM-based swap protocols for use in DeFiActions
19/// workflows.
20///
21access(all) contract UniswapV2SwapConnectors {
22
23 /// Swapper
24 ///
25 /// A DeFiActions connector that swaps between tokens using an EVM-based UniswapV2Router contract
26 ///
27 access(all) struct Swapper : DeFiActions.Swapper {
28 /// UniswapV2Router contract's EVM address
29 access(all) let routerAddress: EVM.EVMAddress
30 /// A swap path defining the route followed for facilitated swaps. Each element should be a valid token address
31 /// for which there is a pool available with the previous and subsequent token address via the defined Router
32 access(all) let addressPath: [EVM.EVMAddress]
33 /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
34 /// specific Identifier to associated connectors on construction
35 access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
36 /// The pre-conversion currency accepted for a swap
37 access(self) let inVault: Type
38 /// The post-conversion currency returned by a swap
39 access(self) let outVault: Type
40 /// An authorized Capability on the CadenceOwnedAccount which this Swapper executes swaps on behalf of
41 access(self) let coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
42
43 init(
44 routerAddress: EVM.EVMAddress,
45 path: [EVM.EVMAddress],
46 inVault: Type,
47 outVault: Type,
48 coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
49 uniqueID: DeFiActions.UniqueIdentifier?
50 ) {
51 pre {
52 path.length >= 2: "Provided path with length of \(path.length) - path must contain at least two EVM addresses)"
53 FlowEVMBridgeConfig.getTypeAssociated(with: path[0]) == inVault:
54 "Provided inVault \(inVault.identifier) is not associated with ERC20 at path[0] \(path[0].toString()) - "
55 .concat("Ensure the type & ERC20 contracts are associated via the VM bridge")
56 FlowEVMBridgeConfig.getTypeAssociated(with: path[path.length - 1]) == outVault:
57 "Provided outVault \(outVault.identifier) is not associated with ERC20 at path[\(path.length - 1)] \(path[path.length - 1].toString()) - "
58 .concat("Ensure the type & ERC20 contracts are associated via the VM bridge")
59 coaCapability.check():
60 "Provided COA Capability is invalid - provided an active, unrevoked Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>"
61 }
62 self.routerAddress = routerAddress
63 self.addressPath = path
64 self.uniqueID = uniqueID
65 self.inVault = inVault
66 self.outVault = outVault
67 self.coaCapability = coaCapability
68 }
69
70 /// Returns a ComponentInfo struct containing information about this Swapper and its inner DFA components
71 ///
72 /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for
73 /// each inner component in the stack.
74 ///
75 access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
76 return DeFiActions.ComponentInfo(
77 type: self.getType(),
78 id: self.uniqueID?.id,
79 innerComponents: []
80 )
81 }
82 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
83 /// a DeFiActions stack. See DeFiActions.align() for more information.
84 ///
85 /// @return a copy of the struct's UniqueIdentifier
86 ///
87 access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
88 return self.uniqueID
89 }
90 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
91 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
92 ///
93 /// @param id: the UniqueIdentifier to set for this component
94 ///
95 access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
96 self.uniqueID = id
97 }
98 /// The type of Vault this Swapper accepts when performing a swap
99 access(all) view fun inType(): Type {
100 return self.inVault
101 }
102 /// The type of Vault this Swapper provides when performing a swap
103 access(all) view fun outType(): Type {
104 return self.outVault
105 }
106 /// The estimated amount required to provide a Vault with the desired output balance returned as a BasicQuote
107 /// struct containing the in and out Vault types and quoted in and out amounts
108 /// NOTE: Cadence only supports decimal precision of 8
109 ///
110 /// @param forDesired: The amount out desired of the post-conversion currency as a result of the swap
111 /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method estimates a swap
112 /// in the opposite direction, outVault -> inVault
113 ///
114 /// @return a SwapConnectors.BasicQuote containing estimate data. In order to prevent upstream reversion,
115 /// result.inAmount and result.outAmount will be 0.0 if an estimate is not available
116 ///
117 access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {DeFiActions.Quote} {
118 let amountIn = self.getAmount(out: false, amount: forDesired, path: reverse ? self.addressPath.reverse() : self.addressPath)
119 return SwapConnectors.BasicQuote(
120 inType: reverse ? self.outType() : self.inType(),
121 outType: reverse ? self.inType() : self.outType(),
122 inAmount: amountIn != nil ? amountIn! : 0.0,
123 outAmount: amountIn != nil ? forDesired : 0.0
124 )
125 }
126 /// The estimated amount delivered out for a provided input balance returned as a BasicQuote returned as a
127 /// BasicQuote struct containing the in and out Vault types and quoted in and out amounts
128 /// NOTE: Cadence only supports decimal precision of 8
129 ///
130 /// @param forProvided: The amount provided of the relevant pre-conversion currency
131 /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method estimates a swap
132 /// in the opposite direction, outVault -> inVault
133 ///
134 /// @return a SwapConnectors.BasicQuote containing estimate data. In order to prevent upstream reversion,
135 /// result.inAmount and result.outAmount will be 0.0 if an estimate is not available
136 ///
137 access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DeFiActions.Quote} {
138 let amountOut = self.getAmount(out: true, amount: forProvided, path: reverse ? self.addressPath.reverse() : self.addressPath)
139 return SwapConnectors.BasicQuote(
140 inType: reverse ? self.outType() : self.inType(),
141 outType: reverse ? self.inType() : self.outType(),
142 inAmount: amountOut != nil ? forProvided : 0.0,
143 outAmount: amountOut != nil ? amountOut! : 0.0
144 )
145 }
146
147 /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. This implementation swaps
148 /// along a path defined on init routing the swap to the pre-defined UniswapV2Router implementation on Flow EVM.
149 /// Any Quote provided defines the amountOutMin value - if none is provided, the current quoted outAmount is
150 /// used.
151 /// NOTE: Cadence only supports decimal precision of 8
152 ///
153 /// @param quote: A `DeFiActions.Quote` data structure. If provided, quote.outAmount is used as the minimum amount out
154 /// desired otherwise a new quote is generated from current state
155 /// @param inVault: Tokens of type `inVault` to swap for a vault of type `outVault`
156 ///
157 /// @return a Vault of type `outVault` containing the swapped currency.
158 ///
159 access(all) fun swap(quote: {DeFiActions.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
160 let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: inVault.balance, reverse: true).outAmount
161 return <-self.swapExactTokensForTokens(exactVaultIn: <-inVault, amountOutMin: amountOutMin, reverse: false)
162 }
163
164 /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose
165 /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted
166 /// to use multiple Flow swap protocols.
167 /// Any Quote provided defines the amountOutMin value - if none is provided, the current quoted outAmount is
168 /// used.
169 /// NOTE: Cadence only supports decimal precision of 8
170 ///
171 /// @param quote: A `DeFiActions.Quote` data structure. If provided, quote.outAmount is used as the minimum amount out
172 /// desired otherwise a new quote is generated from current state
173 /// @param residual: Tokens of type `outVault` to swap back to `inVault`
174 ///
175 /// @return a Vault of type `inVault` containing the swapped currency.
176 ///
177 access(all) fun swapBack(quote: {DeFiActions.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
178 let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: residual.balance, reverse: true).outAmount
179 return <-self.swapExactTokensForTokens(
180 exactVaultIn: <-residual,
181 amountOutMin: amountOutMin,
182 reverse: true
183 )
184 }
185
186 /// Port of UniswapV2Router.swapExactTokensForTokens swapping the exact amount provided along the given path,
187 /// returning the final output Vault
188 ///
189 /// @param exactVaultIn: The pre-conversion currency to swap
190 /// @param amountOutMin: The minimum amount of post-conversion tokens to swap for
191 /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method swaps in the
192 /// opposite direction, outVault -> inVault
193 ///
194 /// @return the resulting Vault containing the swapped tokens
195 ///
196 access(self) fun swapExactTokensForTokens(
197 exactVaultIn: @{FungibleToken.Vault},
198 amountOutMin: UFix64,
199 reverse: Bool
200 ): @{FungibleToken.Vault} {
201 let id = self.uniqueID?.id?.toString() ?? "UNASSIGNED"
202 let idType = self.uniqueID?.getType()?.identifier ?? "UNASSIGNED"
203 let coa = self.borrowCOA()
204 ?? panic("The COA Capability contained by Swapper \(self.getType().identifier) with UniqueIdentifier "
205 .concat("\(idType) ID \(id) is invalid - cannot perform an EVM swap without a valid COA Capability"))
206
207 // withdraw FLOW from the COA to cover the VM bridge fee
208 let bridgeFeeBalance = EVM.Balance(attoflow: 0)
209 bridgeFeeBalance.setFLOW(flow: 2.0 * FlowEVMBridgeUtils.calculateBridgeFee(bytes: 128)) // bridging to EVM then from EVM, hence factor of 2
210 let feeVault <- coa.withdraw(balance: bridgeFeeBalance)
211 let feeVaultRef = &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
212
213 // bridge the provided to the COA's EVM address
214 let inTokenAddress = reverse ? self.addressPath[self.addressPath.length - 1] : self.addressPath[0]
215 let evmAmountIn = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
216 exactVaultIn.balance,
217 erc20Address: inTokenAddress
218 )
219 coa.depositTokens(vault: <-exactVaultIn, feeProvider: feeVaultRef)
220
221 // approve the router to swap tokens
222 var res = self.call(to: inTokenAddress,
223 signature: "approve(address,uint256)",
224 args: [self.routerAddress, evmAmountIn],
225 gasLimit: 15_000_000,
226 value: 0,
227 dryCall: false
228 )!
229 if res.status != EVM.Status.successful {
230 UniswapV2SwapConnectors._callError("approve(address,uint256)",
231 res, inTokenAddress, idType, id, self.getType())
232 }
233 // perform the swap
234 res = self.call(to: self.routerAddress,
235 signature: "swapExactTokensForTokens(uint,uint,address[],address,uint)", // amountIn, amountOutMin, path, to, deadline (timestamp)
236 args: [evmAmountIn, UInt256(0), (reverse ? self.addressPath.reverse() : self.addressPath), coa.address(), UInt256(getCurrentBlock().timestamp)],
237 gasLimit: 15_000_000,
238 value: 0,
239 dryCall: false
240 )!
241 if res.status != EVM.Status.successful {
242 // revert because the funds have already been deposited to the COA - a no-op would leave the funds in EVM
243 UniswapV2SwapConnectors._callError("swapExactTokensForTokens(uint,uint,address[],address,uint)",
244 res, self.routerAddress, idType, id, self.getType())
245 }
246 let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: res.data)
247 let amountsOut = decoded[0] as! [UInt256]
248
249 // withdraw tokens from EVM
250 let outVault <- coa.withdrawTokens(type: self.outType(),
251 amount: amountsOut[amountsOut.length - 1],
252 feeProvider: feeVaultRef
253 )
254
255 // clean up the remaining feeVault & return the swap output Vault
256 self.handleRemainingFeeVault(<-feeVault)
257 return <- outVault
258 }
259
260 /* --- Internal --- */
261
262 /// Internal method used to retrieve router.getAmountsIn and .getAmountsOut estimates. The returned array is the
263 /// estimate returned from the router where each value is a swapped amount corresponding to the swap along the
264 /// provided path.
265 ///
266 /// @param out: If true, getAmountsOut is called, otherwise getAmountsIn is called
267 /// @param amount: The amount in or out. If out is true, the amount will be used as the amount in provided,
268 /// otherwise amount defines the desired amount out for the estimate
269 /// @param path: The path of ERC20 token addresses defining the sequence of swaps executed to arrive at the
270 /// desired token out
271 ///
272 /// @return An estimate of the amounts for each swap along the path. If out is true, the return value contains
273 /// the values in, otherwise the array contains the values out for each swap along the path
274 ///
275 access(self) fun getAmount(out: Bool, amount: UFix64, path: [EVM.EVMAddress]): UFix64? {
276 let callRes = self.call(to: self.routerAddress,
277 signature: out ? "getAmountsOut(uint,address[])" : "getAmountsIn(uint,address[])",
278 args: [amount],
279 gasLimit: 5_000_000,
280 value: UInt(0),
281 dryCall: true
282 )
283 if callRes == nil || callRes!.status != EVM.Status.successful {
284 return nil
285 }
286 let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: callRes!.data) // can revert if the type cannot be decoded
287 let uintAmounts: [UInt256] = decoded.length > 0 ? decoded[0] as! [UInt256] : []
288 if uintAmounts.length == 0 {
289 return nil
290 } else if out {
291 return FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(uintAmounts[uintAmounts.length - 1], erc20Address: path[path.length - 1])
292 } else {
293 return FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(uintAmounts[0], erc20Address: path[0])
294 }
295 }
296
297 /// Deposits any remainder in the provided Vault or burns if it it's empty
298 access(self) fun handleRemainingFeeVault(_ vault: @FlowToken.Vault) {
299 if vault.balance > 0.0 {
300 self.borrowCOA()!.deposit(from: <-vault)
301 } else {
302 Burner.burn(<-vault)
303 }
304 }
305
306 /// Returns a reference to the Swapper's COA or `nil` if the contained Capability is invalid
307 access(self) view fun borrowCOA(): auth(EVM.Owner) &EVM.CadenceOwnedAccount? {
308 return self.coaCapability.borrow()
309 }
310
311 /// Makes a call to the Swapper's routerEVMAddress via the contained COA Capability with the provided signature,
312 /// args, and value. If flagged as dryCall, the more efficient and non-mutating COA.dryCall is used. A result is
313 /// returned as long as the COA Capability is valid, otherwise `nil` is returned.
314 access(self) fun call(
315 to: EVM.EVMAddress,
316 signature: String,
317 args: [AnyStruct],
318 gasLimit: UInt64,
319 value: UInt,
320 dryCall: Bool
321 ): EVM.Result? {
322 let calldata = EVM.encodeABIWithSignature(signature, args)
323 let valueBalance = EVM.Balance(attoflow: value)
324 if let coa = self.borrowCOA() {
325 let res: EVM.Result = dryCall
326 ? coa.dryCall(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
327 : coa.call(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
328 return res
329 }
330 return nil
331 }
332 }
333
334 /// Converts the given amounts from their ERC20 UInt256 to UFix64 amounts according to the ERC20 defined decimals.
335 /// Assumes each EVM address in path is an ERC20 contract
336 access(self)
337 fun _convertEVMAmountsToCadenceAmounts(_ amounts: [UInt256], path: [EVM.EVMAddress]): [UFix64] {
338 let convertedAmounts: [UFix64]= []
339 for i, amount in amounts {
340 convertedAmounts.append(FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(amount, erc20Address: path[i]))
341 }
342 return convertedAmounts
343 }
344
345 /// Reverts with a message constructed from the provided args. Used in the event of a coa.call() error
346 access(self)
347 fun _callError(_ signature: String, _ res: EVM.Result,_ target: EVM.EVMAddress, _ uniqueIDType: String, _ id: String, _ swapperType: Type) {
348 panic("Call to \(target.toString()).\(signature) from Swapper \(swapperType.identifier) "
349 .concat("with UniqueIdentifier \(uniqueIDType) ID \(id) failed: \n\t"
350 .concat("Status value: \(res.status.rawValue)\n\t"))
351 .concat("Error code: \(res.errorCode)\n\t")
352 .concat("ErrorMessage: \(res.errorMessage)\n"))
353 }
354}
355