Smart Contract

YieldPetsUSDCVault

A.73fa40543604c4aa.YieldPetsUSDCVault

Valid From

144,972,275

Deployed

1d ago
Mar 12, 2026, 05:00:18 PM UTC

Dependents

0 imports
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