Smart Contract
UniswapV2SwapConnectors
A.f94f371678513b2b.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 0x6d888f175c158410
10import SwapConnectors from 0xe1a479f0cb911df9
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 uintDesired = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
119 forDesired,
120 erc20Address: reverse ? self.addressPath[0] : self.addressPath[self.addressPath.length - 1]
121 )
122 let amountIn = self.getAmount(out: false, amount: uintDesired, path: reverse ? self.addressPath.reverse() : self.addressPath)
123 return SwapConnectors.BasicQuote(
124 inType: reverse ? self.outType() : self.inType(),
125 outType: reverse ? self.inType() : self.outType(),
126 inAmount: amountIn != nil ? amountIn! : 0.0,
127 outAmount: amountIn != nil ? forDesired : 0.0
128 )
129 }
130 /// The estimated amount delivered out for a provided input balance returned as a BasicQuote returned as a
131 /// BasicQuote struct containing the in and out Vault types and quoted in and out amounts
132 /// NOTE: Cadence only supports decimal precision of 8
133 ///
134 /// @param forProvided: The amount provided of the relevant pre-conversion currency
135 /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method estimates a swap
136 /// in the opposite direction, outVault -> inVault
137 ///
138 /// @return a SwapConnectors.BasicQuote containing estimate data. In order to prevent upstream reversion,
139 /// result.inAmount and result.outAmount will be 0.0 if an estimate is not available
140 ///
141 access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DeFiActions.Quote} {
142 let uintProvided = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
143 forProvided,
144 erc20Address: reverse ? self.addressPath[self.addressPath.length - 1] : self.addressPath[0]
145 )
146 let amountOut = self.getAmount(out: true, amount: uintProvided, path: reverse ? self.addressPath.reverse() : self.addressPath)
147 return SwapConnectors.BasicQuote(
148 inType: reverse ? self.outType() : self.inType(),
149 outType: reverse ? self.inType() : self.outType(),
150 inAmount: amountOut != nil ? forProvided : 0.0,
151 outAmount: amountOut != nil ? amountOut! : 0.0
152 )
153 }
154
155 /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. This implementation swaps
156 /// along a path defined on init routing the swap to the pre-defined UniswapV2Router implementation on Flow EVM.
157 /// Any Quote provided defines the amountOutMin value - if none is provided, the current quoted outAmount is
158 /// used.
159 /// NOTE: Cadence only supports decimal precision of 8
160 ///
161 /// @param quote: A `DeFiActions.Quote` data structure. If provided, quote.outAmount is used as the minimum amount out
162 /// desired otherwise a new quote is generated from current state
163 /// @param inVault: Tokens of type `inVault` to swap for a vault of type `outVault`
164 ///
165 /// @return a Vault of type `outVault` containing the swapped currency.
166 ///
167 access(all) fun swap(quote: {DeFiActions.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
168 let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: inVault.balance, reverse: false).outAmount
169 return <-self.swapExactTokensForTokens(exactVaultIn: <-inVault, amountOutMin: amountOutMin, reverse: false)
170 }
171
172 /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose
173 /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted
174 /// to use multiple Flow swap protocols.
175 /// Any Quote provided defines the amountOutMin value - if none is provided, the current quoted outAmount is
176 /// used.
177 /// NOTE: Cadence only supports decimal precision of 8
178 ///
179 /// @param quote: A `DeFiActions.Quote` data structure. If provided, quote.outAmount is used as the minimum amount out
180 /// desired otherwise a new quote is generated from current state
181 /// @param residual: Tokens of type `outVault` to swap back to `inVault`
182 ///
183 /// @return a Vault of type `inVault` containing the swapped currency.
184 ///
185 access(all) fun swapBack(quote: {DeFiActions.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
186 let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: residual.balance, reverse: true).outAmount
187 return <-self.swapExactTokensForTokens(
188 exactVaultIn: <-residual,
189 amountOutMin: amountOutMin,
190 reverse: true
191 )
192 }
193
194 /// Port of UniswapV2Router.swapExactTokensForTokens swapping the exact amount provided along the given path,
195 /// returning the final output Vault
196 ///
197 /// @param exactVaultIn: The pre-conversion currency to swap
198 /// @param amountOutMin: The minimum amount of post-conversion tokens to swap for
199 /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method swaps in the
200 /// opposite direction, outVault -> inVault
201 ///
202 /// @return the resulting Vault containing the swapped tokens
203 ///
204 access(self) fun swapExactTokensForTokens(
205 exactVaultIn: @{FungibleToken.Vault},
206 amountOutMin: UFix64,
207 reverse: Bool
208 ): @{FungibleToken.Vault} {
209 let id = self.uniqueID?.id?.toString() ?? "UNASSIGNED"
210 let idType = self.uniqueID?.getType()?.identifier ?? "UNASSIGNED"
211 let coa = self.borrowCOA()
212 ?? panic("The COA Capability contained by Swapper \(self.getType().identifier) with UniqueIdentifier "
213 .concat("\(idType) ID \(id) is invalid - cannot perform an EVM swap without a valid COA Capability"))
214
215 // withdraw FLOW from the COA to cover the VM bridge fee
216 let bridgeFeeBalance = EVM.Balance(attoflow: 0)
217 bridgeFeeBalance.setFLOW(flow: 2.0 * FlowEVMBridgeUtils.calculateBridgeFee(bytes: 128)) // bridging to EVM then from EVM, hence factor of 2
218 let feeVault <- coa.withdraw(balance: bridgeFeeBalance)
219 let feeVaultRef = &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
220
221 // bridge the provided to the COA's EVM address
222 let inTokenAddress = reverse ? self.addressPath[self.addressPath.length - 1] : self.addressPath[0]
223 let evmAmountIn = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
224 exactVaultIn.balance,
225 erc20Address: inTokenAddress
226 )
227 coa.depositTokens(vault: <-exactVaultIn, feeProvider: feeVaultRef)
228
229 // approve the router to swap tokens
230 var res = self.call(to: inTokenAddress,
231 signature: "approve(address,uint256)",
232 args: [self.routerAddress, evmAmountIn],
233 gasLimit: 100_000,
234 value: 0,
235 dryCall: false
236 )!
237 if res.status != EVM.Status.successful {
238 UniswapV2SwapConnectors._callError("approve(address,uint256)",
239 res, inTokenAddress, idType, id, self.getType())
240 }
241 // perform the swap
242 res = self.call(to: self.routerAddress,
243 signature: "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)", // amountIn, amountOutMin, path, to, deadline (timestamp)
244 args: [evmAmountIn, UInt256(0), (reverse ? self.addressPath.reverse() : self.addressPath), coa.address(), UInt256(getCurrentBlock().timestamp)],
245 gasLimit: 1_000_000,
246 value: 0,
247 dryCall: false
248 )!
249 if res.status != EVM.Status.successful {
250 // revert because the funds have already been deposited to the COA - a no-op would leave the funds in EVM
251 UniswapV2SwapConnectors._callError("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)",
252 res, self.routerAddress, idType, id, self.getType())
253 }
254 let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: res.data)
255 let amountsOut = decoded[0] as! [UInt256]
256
257 // withdraw tokens from EVM
258 let outVault <- coa.withdrawTokens(type: self.outType(),
259 amount: amountsOut[amountsOut.length - 1],
260 feeProvider: feeVaultRef
261 )
262
263 // clean up the remaining feeVault & return the swap output Vault
264 self.handleRemainingFeeVault(<-feeVault)
265 return <- outVault
266 }
267
268 /* --- Internal --- */
269
270 /// Internal method used to retrieve router.getAmountsIn and .getAmountsOut estimates. The returned array is the
271 /// estimate returned from the router where each value is a swapped amount corresponding to the swap along the
272 /// provided path.
273 ///
274 /// @param out: If true, getAmountsOut is called, otherwise getAmountsIn is called
275 /// @param amount: The amount in or out. If out is true, the amount will be used as the amount in provided,
276 /// otherwise amount defines the desired amount out for the estimate
277 /// @param path: The path of ERC20 token addresses defining the sequence of swaps executed to arrive at the
278 /// desired token out
279 ///
280 /// @return An estimate of the amounts for each swap along the path. If out is true, the return value contains
281 /// the values in, otherwise the array contains the values out for each swap along the path
282 ///
283 access(self) fun getAmount(out: Bool, amount: UInt256, path: [EVM.EVMAddress]): UFix64? {
284 let callRes = self.call(to: self.routerAddress,
285 signature: out ? "getAmountsOut(uint,address[])" : "getAmountsIn(uint,address[])",
286 args: [amount, path],
287 gasLimit: 1_000_000,
288 value: UInt(0),
289 dryCall: true
290 )
291 if callRes == nil || callRes!.status != EVM.Status.successful {
292 return nil
293 }
294 let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: callRes!.data) // can revert if the type cannot be decoded
295 let uintAmounts: [UInt256] = decoded.length > 0 ? decoded[0] as! [UInt256] : []
296 if uintAmounts.length == 0 {
297 return nil
298 } else if out {
299 return FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(uintAmounts[uintAmounts.length - 1], erc20Address: path[path.length - 1])
300 } else {
301 return FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(uintAmounts[0], erc20Address: path[0])
302 }
303 }
304
305 /// Deposits any remainder in the provided Vault or burns if it it's empty
306 access(self) fun handleRemainingFeeVault(_ vault: @FlowToken.Vault) {
307 if vault.balance > 0.0 {
308 self.borrowCOA()!.deposit(from: <-vault)
309 } else {
310 Burner.burn(<-vault)
311 }
312 }
313
314 /// Returns a reference to the Swapper's COA or `nil` if the contained Capability is invalid
315 access(self) view fun borrowCOA(): auth(EVM.Owner) &EVM.CadenceOwnedAccount? {
316 return self.coaCapability.borrow()
317 }
318
319 /// Makes a call to the Swapper's routerEVMAddress via the contained COA Capability with the provided signature,
320 /// args, and value. If flagged as dryCall, the more efficient and non-mutating COA.dryCall is used. A result is
321 /// returned as long as the COA Capability is valid, otherwise `nil` is returned.
322 access(self) fun call(
323 to: EVM.EVMAddress,
324 signature: String,
325 args: [AnyStruct],
326 gasLimit: UInt64,
327 value: UInt,
328 dryCall: Bool
329 ): EVM.Result? {
330 let calldata = EVM.encodeABIWithSignature(signature, args)
331 let valueBalance = EVM.Balance(attoflow: value)
332 if let coa = self.borrowCOA() {
333 let res: EVM.Result = dryCall
334 ? coa.dryCall(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
335 : coa.call(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
336 return res
337 }
338 return nil
339 }
340 }
341
342 /// Reverts with a message constructed from the provided args. Used in the event of a coa.call() error
343 access(self)
344 fun _callError(_ signature: String, _ res: EVM.Result,_ target: EVM.EVMAddress, _ uniqueIDType: String, _ id: String, _ swapperType: Type) {
345 panic("Call to \(target.toString()).\(signature) from Swapper \(swapperType.identifier) "
346 .concat("with UniqueIdentifier \(uniqueIDType) ID \(id) failed: \n\t"
347 .concat("Status value: \(res.status.rawValue)\n\t"))
348 .concat("Error code: \(res.errorCode)\n\t")
349 .concat("ErrorMessage: \(res.errorMessage)\n"))
350 }
351}
352