Smart Contract

DCATransactionHandlerV2

A.ca7ee55e4fc3251a.DCATransactionHandlerV2

Valid From

134,644,347

Deployed

5d ago
Feb 22, 2026, 02:51:37 AM UTC

Dependents

13 imports
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import FlowTransactionSchedulerUtils from 0xe467b9dd11fa00df
3import DCAControllerV2 from 0xca7ee55e4fc3251a
4import DCAPlanV2 from 0xca7ee55e4fc3251a
5import DeFiMath from 0xca7ee55e4fc3251a
6import FungibleToken from 0xf233dcee88fe0abe
7import FlowToken from 0x1654653399040a61
8import SwapRouter from 0xa6850776a94e6551
9import EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14 from 0x1e4aa0b87d10b141
10
11/// DCATransactionHandler: Scheduled transaction handler for DCA execution (USDC/FLOW swaps via IncrementFi)
12///
13/// This contract implements the FlowTransactionScheduler.TransactionHandler interface
14/// to enable autonomous DCA plan execution via Forte Scheduled Transactions.
15///
16/// Architecture:
17/// 1. User creates DCA plan with DCAController
18/// 2. Plan schedules execution via FlowTransactionScheduler
19/// 3. At scheduled time, scheduler calls this handler's executeTransaction()
20/// 4. Handler:
21///    - Validates plan is ready
22///    - Builds DeFi Actions stack (Source → Swapper → Sink)
23///    - Executes swap
24///    - Updates plan accounting
25///    - Reschedules next execution if plan is still active
26///
27/// Educational Notes:
28/// - Implements FlowTransactionScheduler.TransactionHandler interface
29/// - Access control: executeTransaction is access(FlowTransactionScheduler.Execute)
30/// - Uses DeFi Actions for composable swap execution
31/// - Stores metadata via getViews/resolveView for discoverability
32access(all) contract DCATransactionHandlerV2 {
33
34    /// Configuration for scheduling next execution
35    access(all) struct ScheduleConfig {
36        access(all) let schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>
37        access(all) let priority: FlowTransactionScheduler.Priority
38        access(all) let executionEffort: UInt64
39
40        init(
41            schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
42            priority: FlowTransactionScheduler.Priority,
43            executionEffort: UInt64
44        ) {
45            self.schedulerManagerCap = schedulerManagerCap
46            self.priority = priority
47            self.executionEffort = executionEffort
48        }
49    }
50
51    /// Transaction data passed to handler
52    access(all) struct DCATransactionData {
53        access(all) let planId: UInt64
54        access(all) let scheduleConfig: ScheduleConfig
55
56        init(planId: UInt64, scheduleConfig: ScheduleConfig) {
57            self.planId = planId
58            self.scheduleConfig = scheduleConfig
59        }
60    }
61
62    /// Event emitted when handler starts execution
63    access(all) event HandlerExecutionStarted(
64        transactionId: UInt64,
65        planId: UInt64,
66        owner: Address,
67        timestamp: UFix64
68    )
69
70    /// Event emitted when handler completes successfully
71    access(all) event HandlerExecutionCompleted(
72        transactionId: UInt64,
73        planId: UInt64,
74        owner: Address,
75        amountIn: UFix64,
76        amountOut: UFix64,
77        nextExecutionScheduled: Bool,
78        timestamp: UFix64
79    )
80
81    /// Event emitted when handler execution fails
82    access(all) event HandlerExecutionFailed(
83        transactionId: UInt64,
84        planId: UInt64?,
85        owner: Address?,
86        reason: String,
87        timestamp: UFix64
88    )
89
90    /// Event emitted when next execution scheduling fails
91    access(all) event NextExecutionSchedulingFailed(
92        planId: UInt64,
93        owner: Address,
94        reason: String,
95        timestamp: UFix64
96    )
97
98    /// Handler resource that implements the Scheduled Transaction interface
99    ///
100    /// Each user has one instance of this stored in their account.
101    /// The scheduler calls executeTransaction() when a DCA plan is due.
102    access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
103
104        /// Reference to the user's DCA controller
105        /// This capability allows the handler to access and update plans
106        access(self) let controllerCap: Capability<auth(DCAControllerV2.Owner) &DCAControllerV2.Controller>
107
108        init(controllerCap: Capability<auth(DCAControllerV2.Owner) &DCAControllerV2.Controller>) {
109            pre {
110                controllerCap.check(): "Invalid controller capability"
111            }
112            self.controllerCap = controllerCap
113        }
114
115        /// Main execution entrypoint called by FlowTransactionScheduler
116        ///
117        /// @param id: Transaction ID from the scheduler
118        /// @param data: Encoded plan data (contains planId)
119        ///
120        /// This function has restricted access - only FlowTransactionScheduler can call it
121        access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
122            let timestamp = getCurrentBlock().timestamp
123
124            // Parse transaction data
125            let txData = data as! DCATransactionData? ?? panic("Invalid transaction data format")
126            let planId = txData.planId
127            let scheduleConfig = txData.scheduleConfig
128            let ownerAddress = self.controllerCap.address
129
130            emit HandlerExecutionStarted(
131                transactionId: id,
132                planId: planId,
133                owner: ownerAddress,
134                timestamp: timestamp
135            )
136
137            // Borrow controller
138            let controller = self.controllerCap.borrow()
139                ?? panic("Could not borrow DCA controller")
140
141            // Borrow plan
142            let planRef = controller.borrowPlan(id: planId)
143                ?? panic("Could not borrow plan with ID: ".concat(planId.toString()))
144
145            // Validate plan is ready
146            if !planRef.isReadyForExecution() {
147                emit HandlerExecutionFailed(
148                    transactionId: id,
149                    planId: planId,
150                    owner: ownerAddress,
151                    reason: "Plan not ready for execution",
152                    timestamp: timestamp
153                )
154                return
155            }
156
157            // Check max executions
158            if planRef.hasReachedMaxExecutions() {
159                emit HandlerExecutionFailed(
160                    transactionId: id,
161                    planId: planId,
162                    owner: ownerAddress,
163                    reason: "Plan has reached maximum executions",
164                    timestamp: timestamp
165                )
166                return
167            }
168
169            // Get vault capabilities
170            let sourceVaultCap = controller.getSourceVaultCapability()
171                ?? panic("Source vault capability not configured")
172
173            let targetVaultCap = controller.getTargetVaultCapability()
174                ?? panic("Target vault capability not configured")
175
176            // Validate capabilities
177            if !sourceVaultCap.check() {
178                emit HandlerExecutionFailed(
179                    transactionId: id,
180                    planId: planId,
181                    owner: ownerAddress,
182                    reason: "Invalid source vault capability",
183                    timestamp: timestamp
184                )
185                return
186            }
187
188            if !targetVaultCap.check() {
189                emit HandlerExecutionFailed(
190                    transactionId: id,
191                    planId: planId,
192                    owner: ownerAddress,
193                    reason: "Invalid target vault capability",
194                    timestamp: timestamp
195                )
196                return
197            }
198
199            // Execute the swap
200            let result = self.executeSwap(
201                planRef: planRef,
202                sourceVaultCap: sourceVaultCap,
203                targetVaultCap: targetVaultCap
204            )
205
206            if result.success {
207                // Record successful execution
208                planRef.recordExecution(
209                    amountIn: result.amountIn!,
210                    amountOut: result.amountOut!
211                )
212
213                // Schedule next execution if plan is still active
214                var nextScheduled = false
215                if planRef.status == DCAPlanV2.PlanStatus.Active && !planRef.hasReachedMaxExecutions() {
216                    planRef.scheduleNextExecution()
217
218                    // Attempt to schedule next execution via Manager
219                    let schedulingResult = self.scheduleNextExecution(
220                        planId: planId,
221                        nextExecutionTime: planRef.nextExecutionTime,
222                        scheduleConfig: scheduleConfig
223                    )
224
225                    nextScheduled = schedulingResult
226                }
227
228                emit HandlerExecutionCompleted(
229                    transactionId: id,
230                    planId: planId,
231                    owner: ownerAddress,
232                    amountIn: result.amountIn!,
233                    amountOut: result.amountOut!,
234                    nextExecutionScheduled: nextScheduled,
235                    timestamp: timestamp
236                )
237            } else {
238                emit HandlerExecutionFailed(
239                    transactionId: id,
240                    planId: planId,
241                    owner: ownerAddress,
242                    reason: result.errorMessage ?? "Unknown error",
243                    timestamp: timestamp
244                )
245            }
246        }
247
248        /// Execute swap using IncrementFi SwapRouter
249        ///
250        /// This uses IncrementFi's production swap infrastructure to swap tokens.
251        /// Supports USDT ↔ FLOW swaps with slippage protection.
252        ///
253        /// @param planRef: Reference to the DCA plan
254        /// @param sourceVaultCap: Capability to withdraw from source vault
255        /// @param targetVaultCap: Capability to deposit to target vault
256        /// @return ExecutionResult with success status and amounts
257        access(self) fun executeSwap(
258            planRef: &DCAPlanV2.Plan,
259            sourceVaultCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>,
260            targetVaultCap: Capability<&{FungibleToken.Receiver}>
261        ): ExecutionResult {
262            // Get amount to invest
263            let amountIn = planRef.amountPerInterval
264
265            // Borrow source vault and check balance
266            let sourceVault = sourceVaultCap.borrow()
267                ?? panic("Could not borrow source vault")
268
269            if sourceVault.balance < amountIn {
270                return ExecutionResult(
271                    success: false,
272                    amountIn: nil,
273                    amountOut: nil,
274                    errorMessage: "Insufficient balance in source vault. Required: ".concat(amountIn.toString()).concat(", Available: ").concat(sourceVault.balance.toString())
275                )
276            }
277
278            // Withdraw tokens to swap
279            let tokensToSwap <- sourceVault.withdraw(amount: amountIn)
280
281            // Determine swap path based on token types
282            let sourceTypeId = planRef.sourceTokenType.identifier
283            let targetTypeId = planRef.targetTokenType.identifier
284
285            let tokenPath: [String] = []
286            if sourceTypeId.contains("EVMVMBridgedToken") && targetTypeId.contains("FlowToken") {
287                // Bridged EVM Token → FLOW
288                tokenPath.append("A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14")
289                tokenPath.append("A.1654653399040a61.FlowToken")
290            } else if targetTypeId.contains("EVMVMBridgedToken") && sourceTypeId.contains("FlowToken") {
291                // FLOW → Bridged EVM Token
292                tokenPath.append("A.1654653399040a61.FlowToken")
293                tokenPath.append("A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14")
294            } else {
295                // Unsupported swap path
296                destroy tokensToSwap
297                return ExecutionResult(
298                    success: false,
299                    amountIn: nil,
300                    amountOut: nil,
301                    errorMessage: "Unsupported token pair. Only bridged EVM token ↔ FLOW swaps are currently supported."
302                )
303            }
304
305            // Get expected output amount from IncrementFi
306            let expectedAmountsOut = SwapRouter.getAmountsOut(
307                amountIn: amountIn,
308                tokenKeyPath: tokenPath
309            )
310            let expectedAmountOut = expectedAmountsOut[expectedAmountsOut.length - 1]
311
312            // Calculate minimum output with slippage protection
313            // maxSlippageBps is in basis points (100 = 1%)
314            let slippageMultiplier = UInt64(10000) - planRef.maxSlippageBps
315            let minAmountOut = expectedAmountOut * UFix64(slippageMultiplier) / 10000.0
316
317            // Set deadline (5 minutes from now)
318            let deadline = getCurrentBlock().timestamp + 300.0
319
320            // Execute swap via IncrementFi SwapRouter
321            let swappedTokens <- SwapRouter.swapExactTokensForTokens(
322                exactVaultIn: <-tokensToSwap,
323                amountOutMin: minAmountOut,
324                tokenKeyPath: tokenPath,
325                deadline: deadline
326            )
327
328            // Get actual output amount
329            let actualAmountOut = swappedTokens.balance
330
331            // Deposit to target vault
332            let targetVault = targetVaultCap.borrow()
333                ?? panic("Could not borrow target vault")
334
335            targetVault.deposit(from: <-swappedTokens)
336
337            return ExecutionResult(
338                success: true,
339                amountIn: amountIn,
340                amountOut: actualAmountOut,
341                errorMessage: nil
342            )
343        }
344
345        /// Schedule the next execution via FlowTransactionScheduler Manager
346        ///
347        /// This function handles the recursive scheduling pattern using the Manager approach:
348        /// 1. Extract ScheduleConfig from transaction data (contains Manager capability)
349        /// 2. Estimate fees for next execution
350        /// 3. Withdraw FLOW fees from user's controller
351        /// 4. Call manager.scheduleByHandler() to schedule next execution
352        ///
353        /// @param planId: Plan ID to include in transaction data
354        /// @param nextExecutionTime: Timestamp for next execution
355        /// @param scheduleConfig: Configuration containing Manager capability
356        /// @return Bool indicating if scheduling succeeded
357        access(self) fun scheduleNextExecution(
358            planId: UInt64,
359            nextExecutionTime: UFix64?,
360            scheduleConfig: ScheduleConfig
361        ): Bool {
362            let ownerAddress = self.controllerCap.address
363            let timestamp = getCurrentBlock().timestamp
364
365            // Verify nextExecutionTime is provided
366            if nextExecutionTime == nil {
367                emit NextExecutionSchedulingFailed(
368                    planId: planId,
369                    owner: ownerAddress,
370                    reason: "Next execution time not set",
371                    timestamp: timestamp
372                )
373                return false
374            }
375
376            // Prepare transaction data with plan ID and schedule config
377            let transactionData = DCATransactionData(
378                planId: planId,
379                scheduleConfig: scheduleConfig
380            )
381
382            // Estimate fees for the next execution
383            let estimate = FlowTransactionScheduler.estimate(
384                data: transactionData,
385                timestamp: nextExecutionTime!,
386                priority: scheduleConfig.priority,
387                executionEffort: scheduleConfig.executionEffort
388            )
389
390            // Check if estimation was successful
391            assert(
392                estimate.timestamp != nil || scheduleConfig.priority == FlowTransactionScheduler.Priority.Low,
393                message: estimate.error ?? "Fee estimation failed"
394            )
395
396            // Borrow the controller to access fee vault
397            let controller = self.controllerCap.borrow()
398            if controller == nil {
399                emit NextExecutionSchedulingFailed(
400                    planId: planId,
401                    owner: ownerAddress,
402                    reason: "Could not borrow controller",
403                    timestamp: timestamp
404                )
405                return false
406            }
407
408            // Get fee vault capability from controller
409            let feeVaultCap = controller!.getFeeVaultCapability()
410            if feeVaultCap == nil || !feeVaultCap!.check() {
411                emit NextExecutionSchedulingFailed(
412                    planId: planId,
413                    owner: ownerAddress,
414                    reason: "Fee vault capability invalid or not set",
415                    timestamp: timestamp
416                )
417                return false
418            }
419
420            // Withdraw fees
421            let feeVault = feeVaultCap!.borrow()
422            if feeVault == nil {
423                emit NextExecutionSchedulingFailed(
424                    planId: planId,
425                    owner: ownerAddress,
426                    reason: "Could not borrow fee vault",
427                    timestamp: timestamp
428                )
429                return false
430            }
431
432            let feeAmount = estimate.flowFee ?? 0.0
433            if feeVault!.balance < feeAmount {
434                emit NextExecutionSchedulingFailed(
435                    planId: planId,
436                    owner: ownerAddress,
437                    reason: "Insufficient FLOW for fees. Required: ".concat(feeAmount.toString()).concat(", Available: ").concat(feeVault!.balance.toString()),
438                    timestamp: timestamp
439                )
440                return false
441            }
442
443            let fees <- feeVault!.withdraw(amount: feeAmount)
444
445            // Borrow scheduler manager
446            let schedulerManager = scheduleConfig.schedulerManagerCap.borrow()
447            if schedulerManager == nil {
448                destroy fees
449                emit NextExecutionSchedulingFailed(
450                    planId: planId,
451                    owner: ownerAddress,
452                    reason: "Could not borrow scheduler manager",
453                    timestamp: timestamp
454                )
455                return false
456            }
457
458            // Use scheduleByHandler() on the Manager to schedule next execution
459            // This works because the handler was previously scheduled through this Manager
460            let scheduledId = schedulerManager!.scheduleByHandler(
461                handlerTypeIdentifier: self.getType().identifier,
462                handlerUUID: self.uuid,
463                data: transactionData,
464                timestamp: nextExecutionTime!,
465                priority: scheduleConfig.priority,
466                executionEffort: scheduleConfig.executionEffort,
467                fees: <-fees as! @FlowToken.Vault
468            )
469
470            // scheduledId > 0 means success
471            if scheduledId == 0 {
472                emit NextExecutionSchedulingFailed(
473                    planId: planId,
474                    owner: ownerAddress,
475                    reason: "Manager.scheduleByHandler() returned 0 (failed to schedule)",
476                    timestamp: timestamp
477                )
478                return false
479            }
480
481            return true
482        }
483
484        /// Get supported view types (for resource metadata)
485        access(all) view fun getViews(): [Type] {
486            return [Type<StoragePath>(), Type<PublicPath>()]
487        }
488
489        /// Resolve a specific view type
490        access(all) fun resolveView(_ view: Type): AnyStruct? {
491            switch view {
492                case Type<StoragePath>():
493                    return /storage/DCATransactionHandler
494                case Type<PublicPath>():
495                    return /public/DCATransactionHandler
496                default:
497                    return nil
498            }
499        }
500    }
501
502    /// Result struct for swap execution
503    access(all) struct ExecutionResult {
504        access(all) let success: Bool
505        access(all) let amountIn: UFix64?
506        access(all) let amountOut: UFix64?
507        access(all) let errorMessage: String?
508
509        init(success: Bool, amountIn: UFix64?, amountOut: UFix64?, errorMessage: String?) {
510            self.success = success
511            self.amountIn = amountIn
512            self.amountOut = amountOut
513            self.errorMessage = errorMessage
514        }
515    }
516
517    /// Factory function to create a new handler
518    ///
519    /// @param controllerCap: Capability to the user's DCA controller
520    /// @return New handler resource
521    access(all) fun createHandler(
522        controllerCap: Capability<auth(DCAControllerV2.Owner) &DCAControllerV2.Controller>
523    ): @Handler {
524        return <- create Handler(controllerCap: controllerCap)
525    }
526
527    /// Helper function to create schedule configuration
528    access(all) fun createScheduleConfig(
529        schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
530        priority: FlowTransactionScheduler.Priority,
531        executionEffort: UInt64
532    ): ScheduleConfig {
533        return ScheduleConfig(
534            schedulerManagerCap: schedulerManagerCap,
535            priority: priority,
536            executionEffort: executionEffort
537        )
538    }
539
540    /// Helper function to create transaction data
541    access(all) fun createTransactionData(
542        planId: UInt64,
543        scheduleConfig: ScheduleConfig
544    ): DCATransactionData {
545        return DCATransactionData(
546            planId: planId,
547            scheduleConfig: scheduleConfig
548        )
549    }
550}
551