Smart Contract

DCATransactionHandlerUnified

A.ca7ee55e4fc3251a.DCATransactionHandlerUnified

Valid From

135,618,067

Deployed

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

Dependents

4 imports
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 SwapRouter from 0xa6850776a94e6551
8import EVM from 0xe467b9dd11fa00df
9import UniswapV3SwapperConnector from 0xca7ee55e4fc3251a
10import DeFiActions from 0xca7ee55e4fc3251a
11
12/// DCATransactionHandlerUnified: Unified DCA Handler with Auto-Rescheduling
13///
14/// Combines V2Loop's proven pattern with EVM swap support:
15/// - LoopConfig with feeProviderCap in data (scheduler-compatible)
16/// - Token-type detection for swap routing
17/// - IncrementFi for Cadence tokens (USDC), UniswapV3 for EVM tokens (USDF)
18///
19/// Storage: /storage/DCATransactionHandlerUnified
20access(all) contract DCATransactionHandlerUnified {
21
22    /// LoopConfig - proven pattern from CounterLoopTransactionHandler
23    access(all) struct LoopConfig {
24        access(all) let planId: UInt64
25        access(all) let intervalSeconds: UFix64
26        access(all) let schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>
27        access(all) let feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
28        access(all) let priority: FlowTransactionScheduler.Priority
29        access(all) let executionEffort: UInt64
30
31        init(
32            planId: UInt64,
33            intervalSeconds: UFix64,
34            schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
35            feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
36            priority: FlowTransactionScheduler.Priority,
37            executionEffort: UInt64
38        ) {
39            self.planId = planId
40            self.intervalSeconds = intervalSeconds
41            self.schedulerManagerCap = schedulerManagerCap
42            self.feeProviderCap = feeProviderCap
43            self.priority = priority
44            self.executionEffort = executionEffort
45        }
46    }
47
48    access(all) event Executed(
49        transactionId: UInt64,
50        planId: UInt64,
51        amountIn: UFix64,
52        amountOut: UFix64,
53        swapType: String,
54        nextScheduled: Bool
55    )
56
57    access(all) event Failed(
58        transactionId: UInt64,
59        planId: UInt64,
60        reason: String
61    )
62
63    access(all) struct SwapResult {
64        access(all) let success: Bool
65        access(all) let amountIn: UFix64?
66        access(all) let amountOut: UFix64?
67        access(all) let swapType: String
68        access(all) let error: String?
69
70        init(success: Bool, amountIn: UFix64?, amountOut: UFix64?, swapType: String, error: String?) {
71            self.success = success
72            self.amountIn = amountIn
73            self.amountOut = amountOut
74            self.swapType = swapType
75            self.error = error
76        }
77    }
78
79    /// Handler resource
80    access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
81        access(self) let controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>
82
83        init(controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>) {
84            self.controllerCap = controllerCap
85        }
86
87        access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
88            let config = data as! LoopConfig? ?? panic("LoopConfig required")
89
90            // Execute swap with auto-routing
91            let swapResult = self.doSwap(planId: config.planId)
92
93            if !swapResult.success {
94                emit Failed(transactionId: id, planId: config.planId, reason: swapResult.error ?? "swap failed")
95                return
96            }
97
98            // Reschedule if plan still active
99            var nextScheduled = false
100            let controller = self.controllerCap.borrow()
101            if controller != nil {
102                let plan = controller!.borrowPlan(id: config.planId)
103                if plan != nil && plan!.status == DCAPlanUnified.PlanStatus.Active && !plan!.hasReachedMaxExecutions() {
104                    nextScheduled = self.reschedule(config: config, data: data!)
105                }
106            }
107
108            emit Executed(
109                transactionId: id,
110                planId: config.planId,
111                amountIn: swapResult.amountIn ?? 0.0,
112                amountOut: swapResult.amountOut ?? 0.0,
113                swapType: swapResult.swapType,
114                nextScheduled: nextScheduled
115            )
116        }
117
118        /// Execute swap with auto-routing based on token types
119        access(self) fun doSwap(planId: UInt64): SwapResult {
120            let controller = self.controllerCap.borrow() ?? panic("No controller")
121            let plan = controller.borrowPlan(id: planId) ?? panic("No plan")
122
123            if !plan.isReadyForExecution() || plan.hasReachedMaxExecutions() {
124                return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "none", error: "not ready")
125            }
126
127            // Route based on token type
128            if plan.requiresEVM() {
129                return self.executeEVMSwap(controller: controller, plan: plan)
130            } else {
131                return self.executeCadenceSwap(controller: controller, plan: plan)
132            }
133        }
134
135        /// Execute Cadence-native swap via IncrementFi SwapRouter
136        access(self) fun executeCadenceSwap(
137            controller: auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller,
138            plan: &DCAPlanUnified.Plan
139        ): SwapResult {
140            let sourceVaultCap = controller.getSourceVaultCapability() ?? panic("no source")
141            let targetVaultCap = controller.getTargetVaultCapability() ?? panic("no target")
142
143            let sourceVault = sourceVaultCap.borrow() ?? panic("borrow source")
144            let amountIn = plan.amountPerInterval
145
146            if sourceVault.balance < amountIn {
147                return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "Cadence", error: "insufficient balance")
148            }
149
150            let tokensToSwap <- sourceVault.withdraw(amount: amountIn)
151
152            // Build token path for IncrementFi
153            let sourceTypeId = plan.sourceTokenType.identifier
154            let targetTypeId = plan.targetTokenType.identifier
155            var tokenPath: [String] = []
156
157            // USDC token contract on mainnet
158            let usdcTypeId = "A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14"
159            let flowTypeId = "A.1654653399040a61.FlowToken"
160
161            if sourceTypeId.contains("FlowToken") && targetTypeId.contains("EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14") {
162                // FLOW -> USDC
163                tokenPath = [flowTypeId, usdcTypeId]
164            } else if sourceTypeId.contains("EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14") && targetTypeId.contains("FlowToken") {
165                // USDC -> FLOW
166                tokenPath = [usdcTypeId, flowTypeId]
167            } else {
168                destroy tokensToSwap
169                return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "Cadence", error: "unsupported pair for Cadence swap")
170            }
171
172            let expectedOut = SwapRouter.getAmountsOut(amountIn: amountIn, tokenKeyPath: tokenPath)
173            let minOut = expectedOut[expectedOut.length - 1] * (10000.0 - UFix64(plan.maxSlippageBps)) / 10000.0
174
175            let swapped <- SwapRouter.swapExactTokensForTokens(
176                exactVaultIn: <-tokensToSwap,
177                amountOutMin: minOut,
178                tokenKeyPath: tokenPath,
179                deadline: getCurrentBlock().timestamp + 300.0
180            )
181
182            let amountOut = swapped.balance
183            let targetVault = targetVaultCap.borrow() ?? panic("borrow target")
184            targetVault.deposit(from: <-swapped)
185
186            plan.recordExecution(amountIn: amountIn, amountOut: amountOut)
187            plan.scheduleNextExecution()
188
189            return SwapResult(success: true, amountIn: amountIn, amountOut: amountOut, swapType: "Cadence/IncrementFi", error: nil)
190        }
191
192        /// Execute EVM swap via UniswapV3 COA
193        access(self) fun executeEVMSwap(
194            controller: auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller,
195            plan: &DCAPlanUnified.Plan
196        ): SwapResult {
197            let sourceVaultCap = controller.getSourceVaultCapability() ?? panic("no source")
198            let targetVaultCap = controller.getTargetVaultCapability() ?? panic("no target")
199            let coaCap = controller.getCOACapability() ?? panic("no COA for EVM swap")
200
201            let sourceVault = sourceVaultCap.borrow() ?? panic("borrow source")
202            let amountIn = plan.amountPerInterval
203
204            if sourceVault.balance < amountIn {
205                return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "EVM", error: "insufficient balance")
206            }
207
208            let tokensToSwap <- sourceVault.withdraw(amount: amountIn)
209
210            // Determine token path and types for EVM swap
211            let sourceTypeId = plan.sourceTokenType.identifier
212            let targetTypeId = plan.targetTokenType.identifier
213
214            // USDF token contract (EVM-bridged)
215            let usdfEVMAddress = EVM.addressFromString("0x2aabea2058b5ac2d339b163c6ab6f2b6d53aabed")
216            // WFLOW on EVM
217            let wflowEVMAddress = EVM.addressFromString("0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e")
218
219            var tokenPath: [EVM.EVMAddress] = []
220            var inVaultType: Type = plan.sourceTokenType
221            var outVaultType: Type = plan.targetTokenType
222
223            if sourceTypeId.contains("FlowToken") {
224                // FLOW -> USDF
225                tokenPath = [wflowEVMAddress, usdfEVMAddress]
226            } else if targetTypeId.contains("FlowToken") {
227                // USDF -> FLOW
228                tokenPath = [usdfEVMAddress, wflowEVMAddress]
229            } else {
230                destroy tokensToSwap
231                return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "EVM", error: "unsupported pair for EVM swap")
232            }
233
234            // Create swapper with 0.3% fee tier (3000)
235            let swapper <- UniswapV3SwapperConnector.createSwapperWithDefaults(
236                tokenPath: tokenPath,
237                feePath: [3000],
238                inVaultType: inVaultType,
239                outVaultType: outVaultType,
240                coaCapability: coaCap
241            )
242
243            // Get quote with slippage
244            let quote = swapper.getQuote(
245                fromTokenType: inVaultType,
246                toTokenType: outVaultType,
247                amount: amountIn
248            )
249
250            // Apply plan's slippage
251            let adjustedMinAmount = quote.expectedAmount * (10000.0 - UFix64(plan.maxSlippageBps)) / 10000.0
252            let adjustedQuote = DeFiActions.Quote(
253                expectedAmount: quote.expectedAmount,
254                minAmount: adjustedMinAmount,
255                slippageTolerance: UFix64(plan.maxSlippageBps) / 10000.0,
256                deadline: nil,
257                data: quote.data
258            )
259
260            // Execute swap
261            let swapped <- swapper.swap(inVault: <-tokensToSwap, quote: adjustedQuote)
262            let amountOut = swapped.balance
263
264            // Deposit to target
265            let targetVault = targetVaultCap.borrow() ?? panic("borrow target")
266            targetVault.deposit(from: <-swapped)
267
268            // Cleanup swapper
269            destroy swapper
270
271            plan.recordExecution(amountIn: amountIn, amountOut: amountOut)
272            plan.scheduleNextExecution()
273
274            return SwapResult(success: true, amountIn: amountIn, amountOut: amountOut, swapType: "EVM/UniswapV3", error: nil)
275        }
276
277        /// Reschedule - follows official pattern exactly
278        access(self) fun reschedule(config: LoopConfig, data: AnyStruct): Bool {
279            let future = getCurrentBlock().timestamp + config.intervalSeconds
280
281            let estimate = FlowTransactionScheduler.estimate(
282                data: data,
283                timestamp: future,
284                priority: config.priority,
285                executionEffort: config.executionEffort
286            )
287
288            if estimate.timestamp == nil && config.priority != FlowTransactionScheduler.Priority.Low {
289                return false
290            }
291
292            let feeVault = config.feeProviderCap.borrow() ?? panic("fee provider")
293            let fees <- feeVault.withdraw(amount: estimate.flowFee ?? 0.0)
294
295            let manager = config.schedulerManagerCap.borrow() ?? panic("manager")
296
297            manager.scheduleByHandler(
298                handlerTypeIdentifier: self.getType().identifier,
299                handlerUUID: self.uuid,
300                data: data,
301                timestamp: future,
302                priority: config.priority,
303                executionEffort: config.executionEffort,
304                fees: <-fees as! @FlowToken.Vault
305            )
306
307            return true
308        }
309    }
310
311    access(all) fun createHandler(
312        controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>
313    ): @Handler {
314        return <- create Handler(controllerCap: controllerCap)
315    }
316
317    access(all) fun createLoopConfig(
318        planId: UInt64,
319        intervalSeconds: UFix64,
320        schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
321        feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
322        priority: FlowTransactionScheduler.Priority,
323        executionEffort: UInt64
324    ): LoopConfig {
325        return LoopConfig(
326            planId: planId,
327            intervalSeconds: intervalSeconds,
328            schedulerManagerCap: schedulerManagerCap,
329            feeProviderCap: feeProviderCap,
330            priority: priority,
331            executionEffort: executionEffort
332        )
333    }
334}
335