Smart Contract
DCATransactionHandlerEVMLoop
A.ca7ee55e4fc3251a.DCATransactionHandlerEVMLoop
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import FlowTransactionSchedulerUtils from 0xe467b9dd11fa00df
3import DCAControllerUnified from 0xca7ee55e4fc3251a
4import DCAPlanUnified from 0xca7ee55e4fc3251a
5import FungibleToken from 0xf233dcee88fe0abe
6import FlowToken from 0x1654653399040a61
7import EVM from 0xe467b9dd11fa00df
8import DeFiActions from 0xca7ee55e4fc3251a
9import UniswapV3SwapperConnector from 0xca7ee55e4fc3251a
10
11/// DCATransactionHandlerEVMLoop: Self-rescheduling EVM DCA handler
12///
13/// Combines:
14/// - Proven EVM swap logic from EVMMinimal
15/// - Auto-rescheduling pattern from V2Loop (official scaffold)
16/// - High executionEffort (9999) for EVM operations
17///
18/// Storage: /storage/DCATransactionHandlerEVMLoop
19access(all) contract DCATransactionHandlerEVMLoop {
20
21 /// Loop configuration - matches official scaffold pattern
22 access(all) struct LoopConfig {
23 access(all) let planId: UInt64
24 access(all) let intervalSeconds: UFix64
25 access(all) let schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>
26 access(all) let feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
27 access(all) let priority: FlowTransactionScheduler.Priority
28 access(all) let executionEffort: UInt64
29
30 init(
31 planId: UInt64,
32 intervalSeconds: UFix64,
33 schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
34 feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
35 priority: FlowTransactionScheduler.Priority,
36 executionEffort: UInt64
37 ) {
38 self.planId = planId
39 self.intervalSeconds = intervalSeconds
40 self.schedulerManagerCap = schedulerManagerCap
41 self.feeProviderCap = feeProviderCap
42 self.priority = priority
43 self.executionEffort = executionEffort
44 }
45 }
46
47 /// Events
48 access(all) event SwapExecuted(
49 transactionId: UInt64,
50 planId: UInt64,
51 amountIn: UFix64,
52 amountOut: UFix64,
53 swapType: String,
54 nextScheduled: Bool
55 )
56
57 access(all) event SwapFailed(
58 transactionId: UInt64,
59 planId: UInt64,
60 reason: String
61 )
62
63 access(all) event Rescheduled(
64 planId: UInt64,
65 nextExecutionTime: UFix64,
66 newScheduleId: UInt64
67 )
68
69 /// Handler resource
70 access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
71 access(self) let controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>
72
73 init(controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>) {
74 pre { controllerCap.check(): "Invalid controller capability" }
75 self.controllerCap = controllerCap
76 }
77
78 access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
79 // 1. Parse LoopConfig
80 let config = data as? LoopConfig
81 if config == nil {
82 emit SwapFailed(transactionId: id, planId: 0, reason: "Invalid LoopConfig")
83 return
84 }
85 let planId = config!.planId
86
87 // 2. Borrow controller
88 let controller = self.controllerCap.borrow()
89 if controller == nil {
90 emit SwapFailed(transactionId: id, planId: planId, reason: "No controller")
91 return
92 }
93
94 // 3. Borrow plan
95 let plan = controller!.borrowPlan(id: planId)
96 if plan == nil {
97 emit SwapFailed(transactionId: id, planId: planId, reason: "No plan")
98 return
99 }
100
101 // 4. Check plan status
102 if plan!.status != DCAPlanUnified.PlanStatus.Active {
103 emit SwapFailed(transactionId: id, planId: planId, reason: "Plan not active")
104 return
105 }
106
107 if plan!.hasReachedMaxExecutions() {
108 emit SwapFailed(transactionId: id, planId: planId, reason: "Max executions reached")
109 return
110 }
111
112 // 5. Execute EVM swap
113 let swapResult = self.executeEVMSwap(controller: controller!, plan: plan!, transactionId: id)
114
115 if !swapResult.success {
116 emit SwapFailed(transactionId: id, planId: planId, reason: swapResult.error ?? "swap failed")
117 return
118 }
119
120 // 6. Record execution on plan
121 plan!.recordExecution(amountIn: swapResult.amountIn!, amountOut: swapResult.amountOut!)
122 plan!.scheduleNextExecution()
123
124 // 7. Reschedule if plan still active
125 var nextScheduled = false
126 if plan!.status == DCAPlanUnified.PlanStatus.Active && !plan!.hasReachedMaxExecutions() {
127 nextScheduled = self.reschedule(config: config!, data: data!)
128 }
129
130 emit SwapExecuted(
131 transactionId: id,
132 planId: planId,
133 amountIn: swapResult.amountIn!,
134 amountOut: swapResult.amountOut!,
135 swapType: "EVM/UniswapV3",
136 nextScheduled: nextScheduled
137 )
138 }
139
140 /// Execute EVM swap via UniswapV3
141 access(self) fun executeEVMSwap(
142 controller: auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller,
143 plan: &DCAPlanUnified.Plan,
144 transactionId: UInt64
145 ): SwapResult {
146 // Validate EVM requirements
147 if !plan.requiresEVM() {
148 return SwapResult(success: false, amountIn: nil, amountOut: nil, error: "Not an EVM plan")
149 }
150
151 // Get COA
152 let coaCap = controller.getCOACapability()
153 if coaCap == nil || !coaCap!.check() {
154 return SwapResult(success: false, amountIn: nil, amountOut: nil, error: "No COA")
155 }
156
157 // Get source vault
158 let sourceVaultCap = controller.getSourceVaultCapability()
159 if sourceVaultCap == nil || !sourceVaultCap!.check() {
160 return SwapResult(success: false, amountIn: nil, amountOut: nil, error: "No source vault")
161 }
162 let sourceVault = sourceVaultCap!.borrow()!
163
164 // Get target vault
165 let targetVaultCap = controller.getTargetVaultCapability()
166 if targetVaultCap == nil || !targetVaultCap!.check() {
167 return SwapResult(success: false, amountIn: nil, amountOut: nil, error: "No target vault")
168 }
169
170 let amountIn = plan.amountPerInterval
171 if sourceVault.balance < amountIn {
172 return SwapResult(success: false, amountIn: nil, amountOut: nil, error: "Insufficient balance")
173 }
174
175 // Withdraw FLOW for swap
176 let tokensToSwap <- sourceVault.withdraw(amount: amountIn) as! @FlowToken.Vault
177
178 // Setup EVM addresses for USDF
179 let usdfEVMAddress = EVM.addressFromString("0x2aabea2058b5ac2d339b163c6ab6f2b6d53aabed")
180 let wflowEVMAddress = EVM.addressFromString("0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e")
181 let tokenPath: [EVM.EVMAddress] = [wflowEVMAddress, usdfEVMAddress]
182
183 // Create swapper
184 let swapper <- UniswapV3SwapperConnector.createSwapperWithDefaults(
185 tokenPath: tokenPath,
186 feePath: [3000], // 0.3% fee tier
187 inVaultType: plan.sourceTokenType,
188 outVaultType: plan.targetTokenType,
189 coaCapability: coaCap!
190 )
191
192 // Get quote
193 let quote = swapper.getQuote(
194 fromTokenType: plan.sourceTokenType,
195 toTokenType: plan.targetTokenType,
196 amount: amountIn
197 )
198
199 // Apply slippage
200 let minAmount = quote.expectedAmount * (10000.0 - UFix64(plan.maxSlippageBps)) / 10000.0
201 let adjustedQuote = DeFiActions.Quote(
202 expectedAmount: quote.expectedAmount,
203 minAmount: minAmount,
204 slippageTolerance: UFix64(plan.maxSlippageBps) / 10000.0,
205 deadline: nil,
206 data: quote.data
207 )
208
209 // Execute swap
210 let swapped <- swapper.swap(inVault: <-tokensToSwap, quote: adjustedQuote)
211 let amountOut = swapped.balance
212
213 // Deposit to target
214 let targetVault = targetVaultCap!.borrow()!
215 targetVault.deposit(from: <-swapped)
216
217 // Cleanup
218 destroy swapper
219
220 return SwapResult(success: true, amountIn: amountIn, amountOut: amountOut, error: nil)
221 }
222
223 /// Reschedule - follows official scaffold pattern
224 access(self) fun reschedule(config: LoopConfig, data: AnyStruct): Bool {
225 let future = getCurrentBlock().timestamp + config.intervalSeconds
226
227 let estimate = FlowTransactionScheduler.estimate(
228 data: data,
229 timestamp: future,
230 priority: config.priority,
231 executionEffort: config.executionEffort
232 )
233
234 if estimate.timestamp == nil && config.priority != FlowTransactionScheduler.Priority.Low {
235 return false
236 }
237
238 // Get fees from feeProviderCap (official pattern)
239 let feeVault = config.feeProviderCap.borrow()
240 if feeVault == nil {
241 return false
242 }
243
244 // Use 2x fee for EVM operations to ensure enough gas
245 let feeAmount = (estimate.flowFee ?? 0.0) * 2.0
246 if feeVault!.balance < feeAmount {
247 return false
248 }
249
250 let fees <- feeVault!.withdraw(amount: feeAmount)
251
252 let manager = config.schedulerManagerCap.borrow()
253 if manager == nil {
254 // Return the fees if manager not available
255 feeVault!.deposit(from: <-fees)
256 return false
257 }
258
259 let scheduleId = manager!.scheduleByHandler(
260 handlerTypeIdentifier: self.getType().identifier,
261 handlerUUID: self.uuid,
262 data: data,
263 timestamp: future,
264 priority: config.priority,
265 executionEffort: config.executionEffort,
266 fees: <-fees as! @FlowToken.Vault
267 )
268
269 emit Rescheduled(planId: config.planId, nextExecutionTime: future, newScheduleId: scheduleId)
270 return true
271 }
272
273 access(all) view fun getViews(): [Type] {
274 return [Type<StoragePath>()]
275 }
276
277 access(all) fun resolveView(_ view: Type): AnyStruct? {
278 switch view {
279 case Type<StoragePath>():
280 return /storage/DCATransactionHandlerEVMLoop
281 default:
282 return nil
283 }
284 }
285 }
286
287 /// Swap result struct
288 access(all) struct SwapResult {
289 access(all) let success: Bool
290 access(all) let amountIn: UFix64?
291 access(all) let amountOut: UFix64?
292 access(all) let error: String?
293
294 init(success: Bool, amountIn: UFix64?, amountOut: UFix64?, error: String?) {
295 self.success = success
296 self.amountIn = amountIn
297 self.amountOut = amountOut
298 self.error = error
299 }
300 }
301
302 /// Create handler
303 access(all) fun createHandler(
304 controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>
305 ): @Handler {
306 return <- create Handler(controllerCap: controllerCap)
307 }
308
309 /// Create loop config
310 access(all) fun createLoopConfig(
311 planId: UInt64,
312 intervalSeconds: UFix64,
313 schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
314 feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
315 priority: FlowTransactionScheduler.Priority,
316 executionEffort: UInt64
317 ): LoopConfig {
318 return LoopConfig(
319 planId: planId,
320 intervalSeconds: intervalSeconds,
321 schedulerManagerCap: schedulerManagerCap,
322 feeProviderCap: feeProviderCap,
323 priority: priority,
324 executionEffort: executionEffort
325 )
326 }
327}
328