Smart Contract
YieldPetsUSDCVault
A.73fa40543604c4aa.YieldPetsUSDCVault
1import EVM from 0xe467b9dd11fa00df
2
3/// YieldPetsUSDCVault - Cadence contract that deposits stgUSDC into MoreMarkets
4/// lending (Aave V3 fork on Flow EVM) and tracks positions on-chain.
5///
6/// Flow:
7/// 1. User creates a COA (Cadence Owned Account) → gets an EVM address
8/// 2. User sends stgUSDC to that EVM address
9/// 3. This contract calls pool.supply() via the COA to earn ~2% APY
10/// 4. Deposits/withdrawals are tracked in Cadence for pet growth scoring
11access(all) contract YieldPetsUSDCVault {
12
13 // ========================================
14 // Entitlements
15 // ========================================
16
17 access(all) entitlement Manage
18
19 // ========================================
20 // Constants - MoreMarkets (Aave V3) on Flow EVM
21 // ========================================
22
23 access(all) let MOREMARKETS_POOL: String // 0xbC92aaC2DBBF42215248B5688eB3D3d2b32F2c8d
24 access(all) let POOL_DATA_PROVIDER: String // 0x79e71e3c0EDF2B88b0aB38E9A1eF0F6a230e56bf
25 access(all) let STGUSDC: String // 0xf1815bd50389c46847f0bda824ec8da914045d14
26 access(all) let STGUSDC_DECIMALS: UInt8 // 6
27
28 // ========================================
29 // Paths
30 // ========================================
31
32 access(all) let VaultStoragePath: StoragePath
33 access(all) let VaultPublicPath: PublicPath
34
35 // ========================================
36 // Events
37 // ========================================
38
39 access(all) event ContractInitialized()
40 access(all) event VaultPositionCreated(account: Address)
41 access(all) event Deposited(account: Address, amount: UFix64, totalDeposited: UFix64, timestamp: UFix64)
42 access(all) event Withdrawn(account: Address, amount: UFix64, totalDeposited: UFix64, timestamp: UFix64)
43
44 // ========================================
45 // Structs
46 // ========================================
47
48 access(all) struct DepositRecord {
49 access(all) let amount: UFix64
50 access(all) let timestamp: UFix64
51
52 init(amount: UFix64) {
53 self.amount = amount
54 self.timestamp = getCurrentBlock().timestamp
55 }
56 }
57
58 // ========================================
59 // Public Interface (readable by anyone)
60 // ========================================
61
62 access(all) resource interface VaultPositionPublic {
63 access(all) view fun getTotalDeposited(): UFix64
64 access(all) view fun getFirstDepositTimestamp(): UFix64
65 access(all) view fun getLastDepositTimestamp(): UFix64
66 access(all) view fun getDepositCount(): Int
67 access(all) view fun getGrowthScore(): UFix64
68 access(all) fun queryATokenBalance(): UInt256
69 access(all) fun getEVMAddressHex(): String
70 access(all) view fun getDeposits(): [DepositRecord]
71 }
72
73 // ========================================
74 // VaultPosition Resource (one per user)
75 // ========================================
76
77 access(all) resource VaultPosition: VaultPositionPublic {
78 /// Capability to user's COA for EVM calls
79 access(self) let evmCap: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>
80
81 /// Cadence-side tracking
82 access(all) var totalDeposited: UFix64
83 access(all) var deposits: [DepositRecord]
84
85 init(evmCap: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>) {
86 self.evmCap = evmCap
87 self.totalDeposited = 0.0
88 self.deposits = []
89 }
90
91 // ============================
92 // Manage (owner-only) functions
93 // ============================
94
95 /// Deposit stgUSDC into MoreMarkets lending pool.
96 /// stgUSDC must already be in the COA's EVM address.
97 access(Manage) fun deposit(amount: UFix64) {
98 pre {
99 amount > 0.0: "Amount must be positive"
100 }
101
102 let coa = self.evmCap.borrow()
103 ?? panic("YieldPetsUSDCVault: invalid COA capability")
104
105 let pool = EVM.addressFromString(YieldPetsUSDCVault.MOREMARKETS_POOL)
106 let stgusdc = EVM.addressFromString(YieldPetsUSDCVault.STGUSDC)
107 let amountScaled = YieldPetsUSDCVault.ufix64ToUInt256(
108 value: amount,
109 decimals: YieldPetsUSDCVault.STGUSDC_DECIMALS
110 )
111
112 // Step 1: Approve stgUSDC for the MoreMarkets Pool (if needed)
113 YieldPetsUSDCVault.ensureAllowance(
114 coa: coa,
115 token: stgusdc,
116 spender: pool,
117 amount: amountScaled
118 )
119
120 // Step 2: Call pool.supply(asset, amount, onBehalfOf, referralCode)
121 let supplyPayload = EVM.encodeABIWithSignature(
122 "supply(address,uint256,address,uint16)",
123 [stgusdc, amountScaled, coa.address(), UInt256(0)]
124 )
125
126 let res = coa.call(
127 to: pool,
128 data: supplyPayload,
129 gasLimit: 500000,
130 value: EVM.Balance(attoflow: 0)
131 )
132
133 assert(
134 res.status == EVM.Status.successful,
135 message: "YieldPetsUSDCVault: supply failed | Status: "
136 .concat(res.status.rawValue.toString())
137 )
138
139 // Track on-chain
140 self.totalDeposited = self.totalDeposited + amount
141 self.deposits.append(DepositRecord(amount: amount))
142
143 emit Deposited(
144 account: self.owner!.address,
145 amount: amount,
146 totalDeposited: self.totalDeposited,
147 timestamp: getCurrentBlock().timestamp
148 )
149 }
150
151 /// Withdraw a specific amount of stgUSDC from MoreMarkets.
152 /// Tokens are returned to the COA's EVM address.
153 access(Manage) fun withdraw(amount: UFix64) {
154 pre {
155 amount > 0.0: "Amount must be positive"
156 }
157
158 let coa = self.evmCap.borrow()
159 ?? panic("YieldPetsUSDCVault: invalid COA capability")
160
161 let pool = EVM.addressFromString(YieldPetsUSDCVault.MOREMARKETS_POOL)
162 let stgusdc = EVM.addressFromString(YieldPetsUSDCVault.STGUSDC)
163 let amountScaled = YieldPetsUSDCVault.ufix64ToUInt256(
164 value: amount,
165 decimals: YieldPetsUSDCVault.STGUSDC_DECIMALS
166 )
167
168 let withdrawPayload = EVM.encodeABIWithSignature(
169 "withdraw(address,uint256,address)",
170 [stgusdc, amountScaled, coa.address()]
171 )
172
173 let res = coa.call(
174 to: pool,
175 data: withdrawPayload,
176 gasLimit: 500000,
177 value: EVM.Balance(attoflow: 0)
178 )
179
180 assert(
181 res.status == EVM.Status.successful,
182 message: "YieldPetsUSDCVault: withdraw failed | Status: "
183 .concat(res.status.rawValue.toString())
184 )
185
186 // Update tracked principal
187 if amount <= self.totalDeposited {
188 self.totalDeposited = self.totalDeposited - amount
189 } else {
190 self.totalDeposited = 0.0
191 }
192
193 emit Withdrawn(
194 account: self.owner!.address,
195 amount: amount,
196 totalDeposited: self.totalDeposited,
197 timestamp: getCurrentBlock().timestamp
198 )
199 }
200
201 /// Withdraw ALL stgUSDC (principal + accrued yield) from MoreMarkets.
202 /// Uses uint256.max to trigger Aave V3 full withdrawal.
203 access(Manage) fun withdrawAll() {
204 let coa = self.evmCap.borrow()
205 ?? panic("YieldPetsUSDCVault: invalid COA capability")
206
207 let pool = EVM.addressFromString(YieldPetsUSDCVault.MOREMARKETS_POOL)
208 let stgusdc = EVM.addressFromString(YieldPetsUSDCVault.STGUSDC)
209
210 // type(uint256).max signals "withdraw everything" to Aave V3
211 let maxUint256: UInt256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935
212
213 let withdrawPayload = EVM.encodeABIWithSignature(
214 "withdraw(address,uint256,address)",
215 [stgusdc, maxUint256, coa.address()]
216 )
217
218 let res = coa.call(
219 to: pool,
220 data: withdrawPayload,
221 gasLimit: 500000,
222 value: EVM.Balance(attoflow: 0)
223 )
224
225 assert(
226 res.status == EVM.Status.successful,
227 message: "YieldPetsUSDCVault: withdrawAll failed | Status: "
228 .concat(res.status.rawValue.toString())
229 )
230
231 let previousTotal = self.totalDeposited
232 self.totalDeposited = 0.0
233
234 emit Withdrawn(
235 account: self.owner!.address,
236 amount: previousTotal,
237 totalDeposited: 0.0,
238 timestamp: getCurrentBlock().timestamp
239 )
240 }
241
242 // ============================
243 // Public view functions
244 // ============================
245
246 access(all) view fun getTotalDeposited(): UFix64 {
247 return self.totalDeposited
248 }
249
250 access(all) view fun getFirstDepositTimestamp(): UFix64 {
251 if self.deposits.length > 0 {
252 return self.deposits[0].timestamp
253 }
254 return 0.0
255 }
256
257 access(all) view fun getLastDepositTimestamp(): UFix64 {
258 if self.deposits.length > 0 {
259 return self.deposits[self.deposits.length - 1].timestamp
260 }
261 return 0.0
262 }
263
264 access(all) view fun getDepositCount(): Int {
265 return self.deposits.length
266 }
267
268 /// Growth score: log10(1 + principal) * daysLocked
269 /// Matches the formula in YieldPets.cdc for pet evolution
270 access(all) view fun getGrowthScore(): UFix64 {
271 if self.totalDeposited == 0.0 || self.deposits.length == 0 {
272 return 0.0
273 }
274 let now = getCurrentBlock().timestamp
275 let firstDeposit = self.deposits[0].timestamp
276 let daysLocked = (now - firstDeposit) / 86400.0
277
278 let logApprox = YieldPetsUSDCVault.approxLog10(1.0 + self.totalDeposited)
279 return logApprox * daysLocked
280 }
281
282 /// Query the live aToken balance on MoreMarkets (principal + accrued yield).
283 /// Makes two EVM calls: getReserveTokensAddresses → balanceOf(aToken).
284 access(all) fun queryATokenBalance(): UInt256 {
285 let coa = self.evmCap.borrow()
286 ?? panic("YieldPetsUSDCVault: invalid COA capability")
287
288 let dataProvider = EVM.addressFromString(YieldPetsUSDCVault.POOL_DATA_PROVIDER)
289 let stgusdc = EVM.addressFromString(YieldPetsUSDCVault.STGUSDC)
290
291 // Step 1: Get aToken address from PoolDataProvider
292 let getTokensPayload = EVM.encodeABIWithSignature(
293 "getReserveTokensAddresses(address)",
294 [stgusdc]
295 )
296
297 let tokensRes = coa.call(
298 to: dataProvider,
299 data: getTokensPayload,
300 gasLimit: 200000,
301 value: EVM.Balance(attoflow: 0)
302 )
303
304 if tokensRes.status != EVM.Status.successful {
305 return 0
306 }
307
308 let tokenAddrs = EVM.decodeABI(
309 types: [
310 Type<EVM.EVMAddress>(), // aTokenAddress
311 Type<EVM.EVMAddress>(), // stableDebtTokenAddress
312 Type<EVM.EVMAddress>() // variableDebtTokenAddress
313 ],
314 data: tokensRes.data
315 )
316 let aToken = tokenAddrs[0] as! EVM.EVMAddress
317
318 // Step 2: Query aToken.balanceOf(coa)
319 let balancePayload = EVM.encodeABIWithSignature(
320 "balanceOf(address)",
321 [coa.address()]
322 )
323
324 let balanceRes = coa.call(
325 to: aToken,
326 data: balancePayload,
327 gasLimit: 200000,
328 value: EVM.Balance(attoflow: 0)
329 )
330
331 if balanceRes.status != EVM.Status.successful {
332 return 0
333 }
334
335 let decoded = EVM.decodeABI(types: [Type<UInt256>()], data: balanceRes.data)
336 return decoded[0] as! UInt256
337 }
338
339 access(all) view fun getDeposits(): [DepositRecord] {
340 return self.deposits
341 }
342
343 /// Returns the COA's EVM address as hex string.
344 /// Users need this to send stgUSDC to their COA before depositing.
345 access(all) fun getEVMAddressHex(): String {
346 let coa = self.evmCap.borrow()
347 ?? panic("YieldPetsUSDCVault: invalid COA capability")
348 return coa.address().toString()
349 }
350 }
351
352 // ========================================
353 // Contract-Level Helpers
354 // ========================================
355
356 /// Convert UFix64 to UInt256 with the specified number of decimals.
357 /// e.g. ufix64ToUInt256(value: 100.5, decimals: 6) → 100500000
358 access(all) fun ufix64ToUInt256(value: UFix64, decimals: UInt8): UInt256 {
359 let intPart = UInt256(UInt64(value))
360 let fracPart = value - UFix64(UInt64(value))
361
362 // Build 10^decimals
363 var power: UInt256 = 1
364 var i: UInt8 = 0
365 while i < decimals {
366 power = power * 10
367 i = i + 1
368 }
369
370 // whole * 10^decimals
371 var result = intPart * power
372
373 // Add fractional contribution
374 // UFix64 has 8 implicit decimal places, so fracPart * 10^8 is exact
375 if fracPart > 0.0 {
376 let frac8 = UInt256(fracPart * 100000000.0)
377 if decimals >= 8 {
378 var extra: UInt256 = 1
379 var j: UInt8 = 0
380 while j < (decimals - 8) {
381 extra = extra * 10
382 j = j + 1
383 }
384 result = result + frac8 * extra
385 } else {
386 var divisor: UInt256 = 1
387 var j: UInt8 = 0
388 while j < (8 - decimals) {
389 divisor = divisor * 10
390 j = j + 1
391 }
392 result = result + frac8 / divisor
393 }
394 }
395
396 return result
397 }
398
399 /// Ensure ERC-20 allowance for a spender, approving max if insufficient
400 access(all) fun ensureAllowance(
401 coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
402 token: EVM.EVMAddress,
403 spender: EVM.EVMAddress,
404 amount: UInt256
405 ) {
406 let allowanceCalldata = EVM.encodeABIWithSignature(
407 "allowance(address,address)",
408 [coa.address(), spender]
409 )
410
411 let allowanceRes = coa.call(
412 to: token,
413 data: allowanceCalldata,
414 gasLimit: 100000,
415 value: EVM.Balance(attoflow: 0)
416 )
417
418 if allowanceRes.status == EVM.Status.successful {
419 let decoded = EVM.decodeABI(types: [Type<UInt256>()], data: allowanceRes.data)
420 let currentAllowance = decoded[0] as! UInt256
421
422 if currentAllowance < amount {
423 let maxApproval: UInt256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935
424 let approveCalldata = EVM.encodeABIWithSignature(
425 "approve(address,uint256)",
426 [spender, maxApproval]
427 )
428
429 let approveRes = coa.call(
430 to: token,
431 data: approveCalldata,
432 gasLimit: 100000,
433 value: EVM.Balance(attoflow: 0)
434 )
435
436 assert(
437 approveRes.status == EVM.Status.successful,
438 message: "YieldPetsUSDCVault: token approval failed"
439 )
440 }
441 }
442 }
443
444 /// Piecewise linear approximation of log10 for growth score
445 access(all) view fun approxLog10(_ x: UFix64): UFix64 {
446 if x <= 1.0 { return 0.0 }
447 if x <= 10.0 { return (x - 1.0) / 9.0 }
448 if x <= 100.0 { return 1.0 + (x - 10.0) / 90.0 }
449 if x <= 1000.0 { return 2.0 + (x - 100.0) / 900.0 }
450 if x <= 10000.0 { return 3.0 + (x - 1000.0) / 9000.0 }
451 if x <= 100000.0 { return 4.0 + (x - 10000.0) / 90000.0 }
452 return 5.0
453 }
454
455 // ========================================
456 // Factory
457 // ========================================
458
459 access(all) fun createVaultPosition(
460 evmCap: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>
461 ): @VaultPosition {
462 return <- create VaultPosition(evmCap: evmCap)
463 }
464
465 // ========================================
466 // Contract Init
467 // ========================================
468
469 init() {
470 self.MOREMARKETS_POOL = "0xbC92aaC2DBBF42215248B5688eB3D3d2b32F2c8d"
471 self.POOL_DATA_PROVIDER = "0x79e71e3c0EDF2B88b0aB38E9A1eF0F6a230e56bf"
472 self.STGUSDC = "0xf1815bd50389c46847f0bda824ec8da914045d14"
473 self.STGUSDC_DECIMALS = 6
474
475 self.VaultStoragePath = /storage/YieldPetsUSDCVault
476 self.VaultPublicPath = /public/YieldPetsUSDCVault
477
478 emit ContractInitialized()
479 }
480}
481