Smart Contract
DCAControllerV3
A.ca7ee55e4fc3251a.DCAControllerV3
1import DCAPlanV3 from 0xca7ee55e4fc3251a
2import FungibleToken from 0xf233dcee88fe0abe
3import FlowToken from 0x1654653399040a61
4import EVM from 0xe467b9dd11fa00df
5
6/// DCAController: User's DCA management resource
7///
8/// Each user has one DCAController stored in their account that:
9/// - Holds all their DCA plans
10/// - Stores capabilities to their token vaults
11/// - Stores COA capability for EVM swap execution
12/// - Provides public interface for querying plans
13///
14/// Educational Notes:
15/// - One controller per user, stored at /storage/DCAControllerV3
16/// - Controller holds references (not vaults) to user's tokens and COA
17/// - Scheduled handlers borrow capabilities from the controller
18/// - Owner entitlement grants privileged access to handler
19access(all) contract DCAControllerV3 {
20
21 /// Owner entitlement for privileged controller access
22 /// This is required by DCATransactionHandler to update plans
23 access(all) entitlement Owner
24
25 /// Storage paths
26 access(all) let ControllerStoragePath: StoragePath
27 access(all) let ControllerPublicPath: PublicPath
28
29 /// Event emitted when a controller is created
30 access(all) event ControllerCreated(owner: Address)
31
32 /// Event emitted when a plan is added to controller
33 access(all) event PlanAddedToController(owner: Address, planId: UInt64)
34
35 /// Event emitted when a plan is removed from controller
36 access(all) event PlanRemovedFromController(owner: Address, planId: UInt64)
37
38 /// Public interface for reading controller state
39 access(all) resource interface ControllerPublic {
40 access(all) view fun getPlanIds(): [UInt64]
41 access(all) view fun getPlan(id: UInt64): &DCAPlanV3.Plan?
42 access(all) fun getAllPlans(): [DCAPlanV3.PlanDetails]
43 access(all) fun getActivePlans(): [DCAPlanV3.PlanDetails]
44 }
45
46 /// The main Controller resource
47 ///
48 /// Stores all DCA plans for a user and manages vault capabilities.
49 access(all) resource Controller: ControllerPublic {
50 /// Dictionary of all plans owned by this controller
51 access(self) let plans: @{UInt64: DCAPlanV3.Plan}
52
53 /// Capability to withdraw from source token vault (typically FLOW)
54 /// This is used by scheduled handlers to fund DCA executions
55 access(self) var sourceVaultCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>?
56
57 /// Capability to deposit to target token vault (typically USDF)
58 /// This is used by scheduled handlers to deposit acquired tokens
59 access(self) var targetVaultCap: Capability<&{FungibleToken.Receiver}>?
60
61 /// Capability to withdraw FLOW for scheduler fees
62 /// This is used by scheduled handlers to pay for autonomous execution
63 access(self) var feeVaultCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>?
64
65 /// COA (Cadence-Owned Account) capability for EVM operations
66 /// This is used by scheduled handlers to execute swaps on Flow EVM DEXes
67 access(self) var coaCap: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?
68
69 init() {
70 self.plans <- {}
71 self.sourceVaultCap = nil
72 self.targetVaultCap = nil
73 self.feeVaultCap = nil
74 self.coaCap = nil
75 }
76
77 /// Set the source vault capability
78 ///
79 /// This should be called once during setup to give the controller
80 /// permission to withdraw from the user's source token vault.
81 ///
82 /// @param cap: Capability with withdraw auth to source vault
83 access(all) fun setSourceVaultCapability(
84 cap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>
85 ) {
86 pre {
87 cap.check(): "Invalid source vault capability"
88 }
89 self.sourceVaultCap = cap
90 }
91
92 /// Set the target vault capability
93 ///
94 /// This should be called once during setup to give the controller
95 /// permission to deposit to the user's target token vault.
96 ///
97 /// @param cap: Capability to deposit to target vault
98 access(all) fun setTargetVaultCapability(
99 cap: Capability<&{FungibleToken.Receiver}>
100 ) {
101 pre {
102 cap.check(): "Invalid target vault capability"
103 }
104 self.targetVaultCap = cap
105 }
106
107 /// Get source vault capability (for scheduled handler)
108 access(all) fun getSourceVaultCapability(): Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>? {
109 return self.sourceVaultCap
110 }
111
112 /// Get target vault capability (for scheduled handler)
113 access(all) fun getTargetVaultCapability(): Capability<&{FungibleToken.Receiver}>? {
114 return self.targetVaultCap
115 }
116
117 /// Set the fee vault capability
118 ///
119 /// This should be called once during setup to give the controller
120 /// permission to withdraw FLOW from the user's vault to pay scheduler fees.
121 ///
122 /// @param cap: Capability with withdraw auth to FLOW vault
123 access(all) fun setFeeVaultCapability(
124 cap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>
125 ) {
126 pre {
127 cap.check(): "Invalid fee vault capability"
128 }
129 self.feeVaultCap = cap
130 }
131
132 /// Get fee vault capability (for scheduled handler)
133 ///
134 /// Returns capability to withdraw FLOW for scheduler execution fees.
135 access(all) fun getFeeVaultCapability(): Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>? {
136 return self.feeVaultCap
137 }
138
139 /// Set the COA capability
140 ///
141 /// This should be called during setup to give the controller
142 /// permission to use the user's COA for EVM operations.
143 ///
144 /// @param cap: Capability with owner auth to COA
145 access(all) fun setCOACapability(
146 cap: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
147 ) {
148 pre {
149 cap.check(): "Invalid COA capability"
150 }
151 self.coaCap = cap
152 }
153
154 /// Get COA capability (for scheduled handler)
155 ///
156 /// Returns capability to use COA for EVM swap execution.
157 access(all) fun getCOACapability(): Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>? {
158 return self.coaCap
159 }
160
161 /// Add a new plan to this controller
162 ///
163 /// @param plan: The DCA plan resource to add
164 access(all) fun addPlan(plan: @DCAPlanV3.Plan) {
165 let planId = plan.id
166
167 // Ensure no duplicate plan IDs
168 assert(!self.plans.containsKey(planId), message: "Plan already exists")
169
170 self.plans[planId] <-! plan
171
172 // Emit event (note: owner address must be obtained from context)
173 emit PlanAddedToController(owner: self.owner!.address, planId: planId)
174 }
175
176 /// Remove and return a plan from this controller
177 ///
178 /// This allows users to cancel plans or transfer them.
179 ///
180 /// @param id: Plan ID to remove
181 /// @return The removed plan resource
182 access(all) fun removePlan(id: UInt64): @DCAPlanV3.Plan {
183 pre {
184 self.plans.containsKey(id): "Plan does not exist"
185 }
186
187 let plan <- self.plans.remove(key: id)!
188 emit PlanRemovedFromController(owner: self.owner!.address, planId: id)
189 return <- plan
190 }
191
192 /// Borrow a reference to a plan (mutable)
193 ///
194 /// Used by scheduled handlers to update plan state during execution.
195 /// Requires Owner entitlement for privileged access.
196 ///
197 /// @param id: Plan ID
198 /// @return Mutable reference to the plan
199 access(Owner) fun borrowPlan(id: UInt64): &DCAPlanV3.Plan? {
200 return &self.plans[id]
201 }
202
203 // ========================================
204 // Public Interface Implementation
205 // ========================================
206
207 /// Get all plan IDs in this controller
208 access(all) view fun getPlanIds(): [UInt64] {
209 return self.plans.keys
210 }
211
212 /// Get a read-only reference to a specific plan
213 access(all) view fun getPlan(id: UInt64): &DCAPlanV3.Plan? {
214 return &self.plans[id]
215 }
216
217 /// Get details of all plans
218 access(all) fun getAllPlans(): [DCAPlanV3.PlanDetails] {
219 let details: [DCAPlanV3.PlanDetails] = []
220 for id in self.plans.keys {
221 if let plan = &self.plans[id] as &DCAPlanV3.Plan? {
222 details.append(plan.getDetails())
223 }
224 }
225 return details
226 }
227
228 /// Get details of only active plans
229 access(all) fun getActivePlans(): [DCAPlanV3.PlanDetails] {
230 let details: [DCAPlanV3.PlanDetails] = []
231 for id in self.plans.keys {
232 if let plan = &self.plans[id] as &DCAPlanV3.Plan? {
233 if plan.status == DCAPlanV3.PlanStatus.Active {
234 details.append(plan.getDetails())
235 }
236 }
237 }
238 return details
239 }
240
241 /// Check if controller has required capabilities configured
242 access(all) view fun isFullyConfigured(): Bool {
243 if let sourceCap = self.sourceVaultCap {
244 if let targetCap = self.targetVaultCap {
245 if let feeCap = self.feeVaultCap {
246 if let coaCap = self.coaCap {
247 return sourceCap.check() && targetCap.check() && feeCap.check() && coaCap.check()
248 }
249 }
250 }
251 }
252 return false
253 }
254 }
255
256 /// Create a new DCA controller
257 ///
258 /// Users call this once to set up their DCA management resource.
259 access(all) fun createController(): @Controller {
260 return <- create Controller()
261 }
262
263 init() {
264 // V3 uses new storage paths to avoid conflicts with V2
265 self.ControllerStoragePath = /storage/DCAControllerV3
266 self.ControllerPublicPath = /public/DCAControllerV3
267 }
268}
269