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