Smart Contract

DCATransactionHandlerUnifiedSimple

A.ca7ee55e4fc3251a.DCATransactionHandlerUnifiedSimple

Valid From

135,626,319

Deployed

5d ago
Feb 23, 2026, 12:44:35 AM UTC

Dependents

10 imports
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