Smart Contract
DCATransactionHandlerUnifiedSimple
A.ca7ee55e4fc3251a.DCATransactionHandlerUnifiedSimple
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import DCAControllerUnified from 0xca7ee55e4fc3251a
3import DCAPlanUnified from 0xca7ee55e4fc3251a
4import DeFiMath from 0xca7ee55e4fc3251a
5import FungibleToken from 0xf233dcee88fe0abe
6import FlowToken from 0x1654653399040a61
7import SwapRouter from 0xa6850776a94e6551
8import EVM from 0xe467b9dd11fa00df
9import DeFiActions from 0xca7ee55e4fc3251a
10import UniswapV3SwapperConnector from 0xca7ee55e4fc3251a
11import EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14 from 0x1e4aa0b87d10b141
12
13/// DCATransactionHandlerUnifiedSimple: Simplified Unified handler WITHOUT autonomous rescheduling
14///
15/// EXPERIMENT: This handler tests whether the Unified handler execution failures are caused by:
16/// 1. Complex LoopConfig with embedded capabilities
17/// 2. Auto-rescheduling logic
18/// 3. Panic-based error handling
19///
20/// This handler uses:
21/// - DCAControllerUnified (unified controller with optional COA)
22/// - DCAPlanUnified (unified plan with requiresEVM() detection)
23/// - V2Simple's PROVEN patterns (simple data, defensive returns, no auto-reschedule)
24///
25/// Storage: /storage/DCATransactionHandlerUnifiedSimple
26access(all) contract DCATransactionHandlerUnifiedSimple {
27
28 /// Simple transaction data - just the plan ID (like V2Simple)
29 access(all) struct SimpleTransactionData {
30 access(all) let planId: UInt64
31
32 init(planId: UInt64) {
33 self.planId = planId
34 }
35 }
36
37 /// Event emitted when handler starts execution
38 access(all) event HandlerExecutionStarted(
39 transactionId: UInt64,
40 planId: UInt64,
41 owner: Address,
42 requiresEVM: Bool,
43 timestamp: UFix64
44 )
45
46 /// Event emitted when handler completes successfully
47 access(all) event HandlerExecutionCompleted(
48 transactionId: UInt64,
49 planId: UInt64,
50 owner: Address,
51 amountIn: UFix64,
52 amountOut: UFix64,
53 swapType: String,
54 executionCount: UInt64,
55 timestamp: UFix64
56 )
57
58 /// Event emitted when handler execution fails
59 access(all) event HandlerExecutionFailed(
60 transactionId: UInt64,
61 planId: UInt64?,
62 owner: Address?,
63 reason: String,
64 timestamp: UFix64
65 )
66
67 /// Handler resource - simplified version with both Cadence and EVM swap support
68 access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
69
70 /// Reference to the user's DCA controller (Unified)
71 access(self) let controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>
72
73 init(controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>) {
74 pre {
75 controllerCap.check(): "Invalid controller capability"
76 }
77 self.controllerCap = controllerCap
78 }
79
80 /// Main execution entrypoint - SIMPLIFIED (no rescheduling, defensive error handling)
81 access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
82 let timestamp = getCurrentBlock().timestamp
83 let ownerAddress = self.controllerCap.address
84
85 // DEFENSIVE: Parse plan ID - simple format
86 let txData = data as? SimpleTransactionData
87 if txData == nil {
88 emit HandlerExecutionFailed(
89 transactionId: id,
90 planId: nil,
91 owner: ownerAddress,
92 reason: "Invalid transaction data format",
93 timestamp: timestamp
94 )
95 return // Return instead of panic
96 }
97 let planId = txData!.planId
98
99 // DEFENSIVE: Borrow controller
100 let controller = self.controllerCap.borrow()
101 if controller == nil {
102 emit HandlerExecutionFailed(
103 transactionId: id,
104 planId: planId,
105 owner: ownerAddress,
106 reason: "Could not borrow DCA controller",
107 timestamp: timestamp
108 )
109 return // Return instead of panic
110 }
111
112 // DEFENSIVE: Borrow plan
113 let planRef = controller!.borrowPlan(id: planId)
114 if planRef == nil {
115 emit HandlerExecutionFailed(
116 transactionId: id,
117 planId: planId,
118 owner: ownerAddress,
119 reason: "Could not borrow plan",
120 timestamp: timestamp
121 )
122 return // Return instead of panic
123 }
124
125 let requiresEVM = planRef!.requiresEVM()
126
127 emit HandlerExecutionStarted(
128 transactionId: id,
129 planId: planId,
130 owner: ownerAddress,
131 requiresEVM: requiresEVM,
132 timestamp: timestamp
133 )
134
135 // DEFENSIVE: Validate plan is ready
136 if !planRef!.isReadyForExecution() {
137 emit HandlerExecutionFailed(
138 transactionId: id,
139 planId: planId,
140 owner: ownerAddress,
141 reason: "Plan not ready for execution",
142 timestamp: timestamp
143 )
144 return
145 }
146
147 // DEFENSIVE: Check max executions
148 if planRef!.hasReachedMaxExecutions() {
149 emit HandlerExecutionFailed(
150 transactionId: id,
151 planId: planId,
152 owner: ownerAddress,
153 reason: "Plan has reached maximum executions",
154 timestamp: timestamp
155 )
156 return
157 }
158
159 // Execute based on token routing
160 var result: ExecutionResult? = nil
161 if requiresEVM {
162 result = self.executeEVMSwap(controller: controller!, planRef: planRef!)
163 } else {
164 result = self.executeCadenceSwap(controller: controller!, planRef: planRef!)
165 }
166
167 if result!.success {
168 // Record successful execution
169 planRef!.recordExecution(
170 amountIn: result!.amountIn!,
171 amountOut: result!.amountOut!
172 )
173
174 // Update next execution time (but DON'T schedule - external process does that)
175 if planRef!.status == DCAPlanUnified.PlanStatus.Active && !planRef!.hasReachedMaxExecutions() {
176 planRef!.scheduleNextExecution()
177 }
178
179 emit HandlerExecutionCompleted(
180 transactionId: id,
181 planId: planId,
182 owner: ownerAddress,
183 amountIn: result!.amountIn!,
184 amountOut: result!.amountOut!,
185 swapType: requiresEVM ? "EVM/UniswapV3" : "Cadence/IncrementFi",
186 executionCount: planRef!.executionCount,
187 timestamp: timestamp
188 )
189 } else {
190 emit HandlerExecutionFailed(
191 transactionId: id,
192 planId: planId,
193 owner: ownerAddress,
194 reason: result!.errorMessage ?? "Unknown error",
195 timestamp: timestamp
196 )
197 }
198 }
199
200 /// Execute swap using IncrementFi SwapRouter (Cadence path for USDC)
201 access(self) fun executeCadenceSwap(
202 controller: auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller,
203 planRef: &DCAPlanUnified.Plan
204 ): ExecutionResult {
205 let amountIn = planRef.amountPerInterval
206
207 // DEFENSIVE: Get vault capabilities
208 let sourceVaultCap = controller.getSourceVaultCapability()
209 if sourceVaultCap == nil {
210 return ExecutionResult(
211 success: false,
212 amountIn: nil,
213 amountOut: nil,
214 errorMessage: "Source vault capability not configured"
215 )
216 }
217
218 let targetVaultCap = controller.getTargetVaultCapability()
219 if targetVaultCap == nil {
220 return ExecutionResult(
221 success: false,
222 amountIn: nil,
223 amountOut: nil,
224 errorMessage: "Target vault capability not configured"
225 )
226 }
227
228 // DEFENSIVE: Validate capabilities
229 if !sourceVaultCap!.check() {
230 return ExecutionResult(
231 success: false,
232 amountIn: nil,
233 amountOut: nil,
234 errorMessage: "Invalid source vault capability"
235 )
236 }
237
238 if !targetVaultCap!.check() {
239 return ExecutionResult(
240 success: false,
241 amountIn: nil,
242 amountOut: nil,
243 errorMessage: "Invalid target vault capability"
244 )
245 }
246
247 // Borrow source vault
248 let sourceVault = sourceVaultCap!.borrow()!
249
250 if sourceVault.balance < amountIn {
251 return ExecutionResult(
252 success: false,
253 amountIn: nil,
254 amountOut: nil,
255 errorMessage: "Insufficient balance"
256 )
257 }
258
259 // Withdraw tokens
260 let tokensToSwap <- sourceVault.withdraw(amount: amountIn)
261
262 // Determine swap path
263 let sourceTypeId = planRef.sourceTokenType.identifier
264 let targetTypeId = planRef.targetTokenType.identifier
265
266 let tokenPath: [String] = []
267 if sourceTypeId.contains("EVMVMBridgedToken") && targetTypeId.contains("FlowToken") {
268 // USDC → FLOW
269 tokenPath.append("A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14")
270 tokenPath.append("A.1654653399040a61.FlowToken")
271 } else if targetTypeId.contains("EVMVMBridgedToken") && sourceTypeId.contains("FlowToken") {
272 // FLOW → USDC
273 tokenPath.append("A.1654653399040a61.FlowToken")
274 tokenPath.append("A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14")
275 } else {
276 destroy tokensToSwap
277 return ExecutionResult(
278 success: false,
279 amountIn: nil,
280 amountOut: nil,
281 errorMessage: "Unsupported token pair for Cadence swap"
282 )
283 }
284
285 // Get expected output
286 let expectedAmountsOut = SwapRouter.getAmountsOut(
287 amountIn: amountIn,
288 tokenKeyPath: tokenPath
289 )
290 let expectedAmountOut = expectedAmountsOut[expectedAmountsOut.length - 1]
291
292 // Calculate minimum with slippage
293 let slippageMultiplier = UInt64(10000) - planRef.maxSlippageBps
294 let minAmountOut = expectedAmountOut * UFix64(slippageMultiplier) / 10000.0
295
296 let deadline = getCurrentBlock().timestamp + 300.0
297
298 // Execute swap
299 let swappedTokens <- SwapRouter.swapExactTokensForTokens(
300 exactVaultIn: <-tokensToSwap,
301 amountOutMin: minAmountOut,
302 tokenKeyPath: tokenPath,
303 deadline: deadline
304 )
305
306 let actualAmountOut = swappedTokens.balance
307
308 // Deposit to target
309 let targetVault = targetVaultCap!.borrow()!
310 targetVault.deposit(from: <-swappedTokens)
311
312 return ExecutionResult(
313 success: true,
314 amountIn: amountIn,
315 amountOut: actualAmountOut,
316 errorMessage: nil
317 )
318 }
319
320 /// Execute swap using UniswapV3 via COA (EVM path for USDF)
321 access(self) fun executeEVMSwap(
322 controller: auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller,
323 planRef: &DCAPlanUnified.Plan
324 ): ExecutionResult {
325 let amountIn = planRef.amountPerInterval
326
327 // DEFENSIVE: Check COA capability
328 let coaCap = controller.getCOACapability()
329 if coaCap == nil {
330 return ExecutionResult(
331 success: false,
332 amountIn: nil,
333 amountOut: nil,
334 errorMessage: "COA capability not configured - required for EVM swaps"
335 )
336 }
337
338 if !coaCap!.check() {
339 return ExecutionResult(
340 success: false,
341 amountIn: nil,
342 amountOut: nil,
343 errorMessage: "Invalid COA capability"
344 )
345 }
346
347 // DEFENSIVE: Get vault capabilities
348 let sourceVaultCap = controller.getSourceVaultCapability()
349 if sourceVaultCap == nil {
350 return ExecutionResult(
351 success: false,
352 amountIn: nil,
353 amountOut: nil,
354 errorMessage: "Source vault capability not configured"
355 )
356 }
357
358 let targetVaultCap = controller.getTargetVaultCapability()
359 if targetVaultCap == nil {
360 return ExecutionResult(
361 success: false,
362 amountIn: nil,
363 amountOut: nil,
364 errorMessage: "Target vault capability not configured"
365 )
366 }
367
368 // DEFENSIVE: Validate capabilities
369 if !sourceVaultCap!.check() {
370 return ExecutionResult(
371 success: false,
372 amountIn: nil,
373 amountOut: nil,
374 errorMessage: "Invalid source vault capability"
375 )
376 }
377
378 if !targetVaultCap!.check() {
379 return ExecutionResult(
380 success: false,
381 amountIn: nil,
382 amountOut: nil,
383 errorMessage: "Invalid target vault capability"
384 )
385 }
386
387 // Borrow source vault
388 let sourceVault = sourceVaultCap!.borrow()!
389
390 if sourceVault.balance < amountIn {
391 return ExecutionResult(
392 success: false,
393 amountIn: nil,
394 amountOut: nil,
395 errorMessage: "Insufficient balance"
396 )
397 }
398
399 // Withdraw FLOW tokens
400 let tokensToSwap <- sourceVault.withdraw(amount: amountIn) as! @FlowToken.Vault
401
402 // Determine token path and types for EVM swap
403 let sourceTypeId = planRef.sourceTokenType.identifier
404 let targetTypeId = planRef.targetTokenType.identifier
405
406 // USDF token contract (EVM-bridged)
407 let usdfEVMAddress = EVM.addressFromString("0x2aabea2058b5ac2d339b163c6ab6f2b6d53aabed")
408 // WFLOW on EVM
409 let wflowEVMAddress = EVM.addressFromString("0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e")
410
411 var tokenPath: [EVM.EVMAddress] = []
412 var inVaultType: Type = planRef.sourceTokenType
413 var outVaultType: Type = planRef.targetTokenType
414
415 if sourceTypeId.contains("FlowToken") {
416 // FLOW -> USDF
417 tokenPath = [wflowEVMAddress, usdfEVMAddress]
418 } else if targetTypeId.contains("FlowToken") {
419 // USDF -> FLOW
420 tokenPath = [usdfEVMAddress, wflowEVMAddress]
421 } else {
422 destroy tokensToSwap
423 return ExecutionResult(
424 success: false,
425 amountIn: nil,
426 amountOut: nil,
427 errorMessage: "Unsupported pair for EVM swap"
428 )
429 }
430
431 // Create swapper with 0.3% fee tier (3000)
432 let swapper <- UniswapV3SwapperConnector.createSwapperWithDefaults(
433 tokenPath: tokenPath,
434 feePath: [3000],
435 inVaultType: inVaultType,
436 outVaultType: outVaultType,
437 coaCapability: coaCap!
438 )
439
440 // Get quote with slippage
441 let quote = swapper.getQuote(
442 fromTokenType: inVaultType,
443 toTokenType: outVaultType,
444 amount: amountIn
445 )
446
447 // Apply plan's slippage
448 let adjustedMinAmount = quote.expectedAmount * (10000.0 - UFix64(planRef.maxSlippageBps)) / 10000.0
449 let adjustedQuote = DeFiActions.Quote(
450 expectedAmount: quote.expectedAmount,
451 minAmount: adjustedMinAmount,
452 slippageTolerance: UFix64(planRef.maxSlippageBps) / 10000.0,
453 deadline: nil,
454 data: quote.data
455 )
456
457 // Execute swap
458 let swapped <- swapper.swap(inVault: <-tokensToSwap, quote: adjustedQuote)
459 let amountOut = swapped.balance
460
461 // Deposit to target
462 let targetVault = targetVaultCap!.borrow()!
463 targetVault.deposit(from: <-swapped)
464
465 // Cleanup swapper
466 destroy swapper
467
468 return ExecutionResult(
469 success: true,
470 amountIn: amountIn,
471 amountOut: amountOut,
472 errorMessage: nil
473 )
474 }
475
476 access(all) view fun getViews(): [Type] {
477 return [Type<StoragePath>(), Type<PublicPath>()]
478 }
479
480 access(all) fun resolveView(_ view: Type): AnyStruct? {
481 switch view {
482 case Type<StoragePath>():
483 return /storage/DCATransactionHandlerUnifiedSimple
484 case Type<PublicPath>():
485 return /public/DCATransactionHandlerUnifiedSimple
486 default:
487 return nil
488 }
489 }
490 }
491
492 /// Result struct for swap execution
493 access(all) struct ExecutionResult {
494 access(all) let success: Bool
495 access(all) let amountIn: UFix64?
496 access(all) let amountOut: UFix64?
497 access(all) let errorMessage: String?
498
499 init(success: Bool, amountIn: UFix64?, amountOut: UFix64?, errorMessage: String?) {
500 self.success = success
501 self.amountIn = amountIn
502 self.amountOut = amountOut
503 self.errorMessage = errorMessage
504 }
505 }
506
507 /// Factory function
508 access(all) fun createHandler(
509 controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>
510 ): @Handler {
511 return <- create Handler(controllerCap: controllerCap)
512 }
513
514 /// Helper to create transaction data
515 access(all) fun createTransactionData(planId: UInt64): SimpleTransactionData {
516 return SimpleTransactionData(planId: planId)
517 }
518}
519