Smart Contract
TestHelpers
A.a092c4aab33daeda.TestHelpers
1import FungibleToken from 0xf233dcee88fe0abe
2import DeFiActions from 0x92195d814edf9cb0
3
4/// TestHelpers - Production-Safe Vault Connector for Testing
5///
6/// This contract provides secure mock implementations of DeFiActions interfaces
7/// for testing purposes. It follows the same security pattern as FlowYieldVaultsConnector.
8///
9/// SECURITY MODEL:
10/// - Vault capabilities are stored in a Resource (VaultManager), NOT in structs
11/// - Resources cannot be copied, protecting the underlying vault capabilities
12/// - The Connector struct only holds a capability to the VaultManager resource
13/// - Only the VaultManager owner (account) can create Connectors
14/// - Public interface only exposes safe operations (deposit, balance check)
15/// - Withdraw operations require entitled capability that can be revoked
16/// - All operations emit events for transparency and monitoring
17///
18/// USAGE:
19/// 1. Call createVaultManagerAndConnector() with your account to set up the manager
20/// 2. Use the returned VaultConnector with PrizeSavings
21/// 3. Fund the manager's underlying vault for testing
22///
23/// NOTE: While this is production-safe, it should primarily be used for testing.
24/// For production yield sources, use proper DeFi integrations like FlowYieldVaultsConnector.
25///
26access(all) contract TestHelpers {
27
28 // Entitlement for withdraw operations - only entitled references can withdraw
29 access(all) entitlement Withdraw
30
31 // Storage paths
32 access(all) let VaultManagerStoragePath: StoragePath
33 access(all) let VaultManagerPublicPath: PublicPath
34
35 // Events for transparency and monitoring
36 access(all) event VaultManagerCreated(ownerAddress: Address, vaultType: String)
37 access(all) event ConnectorCreated(managerAddress: Address, vaultType: String)
38 access(all) event DepositedToManager(managerAddress: Address, amount: UFix64, vaultType: String)
39 access(all) event WithdrawnFromManager(managerAddress: Address, amount: UFix64, vaultType: String)
40
41 /// Public interface - exposes only safe operations
42 /// Anyone can deposit funds and check balance, but cannot withdraw
43 access(all) resource interface VaultManagerPublic {
44 access(all) view fun getBalance(): UFix64
45 access(all) view fun getVaultType(): Type
46 access(all) fun deposit(from: @{FungibleToken.Vault})
47 }
48
49 /// VaultManager Resource
50 ///
51 /// SECURITY: This resource holds the sensitive vault capabilities.
52 /// Resources cannot be copied, so the capabilities are protected.
53 /// Only entitled references can call withdrawFunds.
54 ///
55 access(all) resource VaultManager: VaultManagerPublic {
56 /// Private: Vault provider capability - can withdraw funds
57 access(self) let providerCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider, FungibleToken.Balance}>
58
59 /// Private: Vault receiver capability - can receive deposits
60 access(self) let receiverCap: Capability<&{FungibleToken.Receiver}>
61
62 /// Public: Vault type for type checking
63 access(all) let vaultType: Type
64
65 /// Public: Owner address for monitoring
66 access(all) let ownerAddress: Address
67
68 init(
69 providerCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider, FungibleToken.Balance}>,
70 receiverCap: Capability<&{FungibleToken.Receiver}>,
71 vaultType: Type,
72 ownerAddress: Address
73 ) {
74 pre {
75 providerCap.check(): "Invalid provider capability"
76 receiverCap.check(): "Invalid receiver capability"
77 }
78 self.providerCap = providerCap
79 self.receiverCap = receiverCap
80 self.vaultType = vaultType
81 self.ownerAddress = ownerAddress
82 }
83
84 /// Get current balance - SAFE: read-only operation
85 access(all) view fun getBalance(): UFix64 {
86 if let provider = self.providerCap.borrow() {
87 return provider.balance
88 }
89 return 0.0
90 }
91
92 /// Get vault type - SAFE: read-only operation
93 access(all) view fun getVaultType(): Type {
94 return self.vaultType
95 }
96
97 /// Deposit funds - SAFE: adding funds doesn't risk existing funds
98 access(all) fun deposit(from: @{FungibleToken.Vault}) {
99 pre {
100 from.getType() == self.vaultType: "Vault type mismatch: expected ".concat(self.vaultType.identifier)
101 }
102 let amount = from.balance
103 let receiver = self.receiverCap.borrow() ?? panic("Cannot borrow receiver capability")
104 receiver.deposit(from: <- from)
105 emit DepositedToManager(
106 managerAddress: self.ownerAddress,
107 amount: amount,
108 vaultType: self.vaultType.identifier
109 )
110 }
111
112 /// Withdraw funds - PROTECTED: requires Withdraw entitlement
113 /// Only callable through an entitled reference, which can only be issued
114 /// by the account that stores this resource.
115 access(Withdraw) fun withdrawFunds(maxAmount: UFix64): @{FungibleToken.Vault} {
116 let provider = self.providerCap.borrow()
117 ?? panic("Cannot borrow provider capability")
118 let available = provider.balance
119 let toWithdraw = maxAmount < available ? maxAmount : available
120
121 if toWithdraw == 0.0 {
122 // Return empty vault of correct type
123 return <- provider.withdraw(amount: 0.0)
124 }
125
126 let vault <- provider.withdraw(amount: toWithdraw)
127 emit WithdrawnFromManager(
128 managerAddress: self.ownerAddress,
129 amount: toWithdraw,
130 vaultType: self.vaultType.identifier
131 )
132 return <- vault
133 }
134 }
135
136 /// VaultConnector Struct
137 ///
138 /// Implements DeFiActions.Sink and DeFiActions.Source for use with PrizeSavings.
139 ///
140 /// SECURITY:
141 /// - Only holds a capability to VaultManager, NOT the raw vault capability
142 /// - Deposit uses public capability (safe - anyone can deposit)
143 /// - Withdraw uses entitled capability (protected - requires Withdraw entitlement)
144 /// - The entitled capability can only be issued by the VaultManager owner
145 ///
146 access(all) struct VaultConnector: DeFiActions.Sink, DeFiActions.Source {
147 /// Address of the account that owns the VaultManager resource
148 access(all) let managerAddress: Address
149
150 /// Entitled capability to VaultManager for withdraw operations
151 /// This can only be issued by the account that stores the VaultManager
152 access(self) let withdrawCap: Capability<auth(Withdraw) &VaultManager>
153
154 /// Vault type for interface compliance
155 access(all) let vaultType: Type
156
157 /// DeFiActions unique ID
158 access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
159
160 init(
161 managerAddress: Address,
162 withdrawCap: Capability<auth(Withdraw) &VaultManager>,
163 vaultType: Type
164 ) {
165 pre {
166 withdrawCap.check(): "Invalid withdraw capability"
167 }
168 self.managerAddress = managerAddress
169 self.withdrawCap = withdrawCap
170 self.vaultType = vaultType
171 self.uniqueID = nil
172 }
173
174 // ============================================================
175 // DeFiActions.Sink Implementation
176 // ============================================================
177
178 /// Deposit funds into the managed vault
179 /// Uses PUBLIC capability - safe operation
180 access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
181 let managerAccount = getAccount(self.managerAddress)
182 let managerRef = managerAccount.capabilities.borrow<&{VaultManagerPublic}>(
183 TestHelpers.VaultManagerPublicPath
184 ) ?? panic("Cannot borrow VaultManager from ".concat(self.managerAddress.toString()))
185
186 let amount = from.balance
187 managerRef.deposit(from: <- from.withdraw(amount: amount))
188 }
189
190 access(all) view fun getSinkType(): Type {
191 return self.vaultType
192 }
193
194 access(all) view fun minimumCapacity(): UFix64 {
195 return 0.0
196 }
197
198 // ============================================================
199 // DeFiActions.Source Implementation
200 // ============================================================
201
202 /// Get available balance
203 /// Uses PUBLIC capability - safe read-only operation
204 access(all) view fun minimumAvailable(): UFix64 {
205 let managerAccount = getAccount(self.managerAddress)
206 if let managerRef = managerAccount.capabilities.borrow<&{VaultManagerPublic}>(
207 TestHelpers.VaultManagerPublicPath
208 ) {
209 return managerRef.getBalance()
210 }
211 return 0.0
212 }
213
214 /// Withdraw funds from the managed vault
215 /// Uses ENTITLED capability - protected operation
216 access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
217 let managerRef = self.withdrawCap.borrow()
218 ?? panic("Cannot borrow VaultManager with Withdraw entitlement. Capability may have been revoked.")
219 return <- managerRef.withdrawFunds(maxAmount: maxAmount)
220 }
221
222 access(all) view fun getSourceType(): Type {
223 return self.vaultType
224 }
225
226 // ============================================================
227 // DeFiActions Component Interface
228 // ============================================================
229
230 access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
231 return DeFiActions.ComponentInfo(
232 type: self.getType(),
233 id: self.uniqueID?.id,
234 innerComponents: []
235 )
236 }
237
238 access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
239 return self.uniqueID
240 }
241
242 access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
243 self.uniqueID = id
244 }
245 }
246
247 // ============================================================
248 // Factory Functions
249 // ============================================================
250
251 /// Create a VaultManager resource and store it in the caller's account.
252 /// Returns a VaultConnector that can be used with PrizeSavings.
253 ///
254 /// SECURITY: This function requires auth(Storage, Capabilities) which means
255 /// only the account owner can call it through a transaction they sign.
256 ///
257 /// @param account: The account to store the VaultManager in (must be signer)
258 /// @param providerCap: Capability to withdraw from the vault (will be protected)
259 /// @param receiverCap: Capability to deposit into the vault
260 /// @param vaultType: Type of the fungible token vault
261 /// @return VaultConnector struct for use with PrizeSavings
262 ///
263 access(all) fun createVaultManagerAndConnector(
264 account: auth(Storage, Capabilities) &Account,
265 providerCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider, FungibleToken.Balance}>,
266 receiverCap: Capability<&{FungibleToken.Receiver}>,
267 vaultType: Type
268 ): VaultConnector {
269 pre {
270 providerCap.check(): "Invalid provider capability"
271 receiverCap.check(): "Invalid receiver capability"
272 account.storage.borrow<&VaultManager>(from: self.VaultManagerStoragePath) == nil:
273 "VaultManager already exists at this storage path"
274 }
275
276 // Create and store the VaultManager resource
277 let manager <- create VaultManager(
278 providerCap: providerCap,
279 receiverCap: receiverCap,
280 vaultType: vaultType,
281 ownerAddress: account.address
282 )
283
284 account.storage.save(<-manager, to: self.VaultManagerStoragePath)
285
286 // Issue and publish PUBLIC capability (deposit and balance only)
287 let publicCap = account.capabilities.storage.issue<&{VaultManagerPublic}>(
288 self.VaultManagerStoragePath
289 )
290 account.capabilities.publish(publicCap, at: self.VaultManagerPublicPath)
291
292 // Issue ENTITLED capability for withdrawals (NOT published publicly)
293 // This capability is stored in the returned connector
294 let withdrawCap = account.capabilities.storage.issue<auth(Withdraw) &VaultManager>(
295 self.VaultManagerStoragePath
296 )
297
298 emit VaultManagerCreated(
299 ownerAddress: account.address,
300 vaultType: vaultType.identifier
301 )
302 emit ConnectorCreated(
303 managerAddress: account.address,
304 vaultType: vaultType.identifier
305 )
306
307 return VaultConnector(
308 managerAddress: account.address,
309 withdrawCap: withdrawCap,
310 vaultType: vaultType
311 )
312 }
313
314 /// Create an additional connector for an existing VaultManager.
315 /// Useful if you need to use the same vault for multiple pools.
316 ///
317 /// SECURITY: Requires auth(Capabilities) - only account owner can create connectors
318 ///
319 access(all) fun createAdditionalConnector(
320 account: auth(Capabilities) &Account
321 ): VaultConnector {
322 // Verify the account has a VaultManager by borrowing public capability
323 let publicCap = account.capabilities.get<&{VaultManagerPublic}>(self.VaultManagerPublicPath)
324 let managerRef = publicCap.borrow()
325 ?? panic("No VaultManager found. Call createVaultManagerAndConnector first.")
326
327 let vaultType = managerRef.getVaultType()
328
329 // Issue a new entitled capability for this connector
330 let withdrawCap = account.capabilities.storage.issue<auth(Withdraw) &VaultManager>(
331 self.VaultManagerStoragePath
332 )
333
334 emit ConnectorCreated(
335 managerAddress: account.address,
336 vaultType: vaultType.identifier
337 )
338
339 return VaultConnector(
340 managerAddress: account.address,
341 withdrawCap: withdrawCap,
342 vaultType: vaultType
343 )
344 }
345
346 // ============================================================
347 // Contract Initialization
348 // ============================================================
349
350 init() {
351 // Use contract address in path identifier for uniqueness
352 let identifier = "testHelpersVaultManager_".concat(self.account.address.toString())
353 self.VaultManagerStoragePath = StoragePath(identifier: identifier)!
354 self.VaultManagerPublicPath = PublicPath(identifier: identifier)!
355 }
356}
357