Smart Contract

DCATransactionHandlerEVMLoop

A.ca7ee55e4fc3251a.DCATransactionHandlerEVMLoop

Valid From

135,630,475

Deployed

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

Dependents

6 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 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