Smart Contract

DriverzStaking

A.f887ece39166906e.DriverzStaking

Deployed

1d ago
Feb 26, 2026, 09:44:38 PM UTC

Dependents

0 imports
1import NonFungibleToken from 0x1d7e57aa55817448
2import VroomToken from 0xf887ece39166906e
3import FungibleToken from 0xf233dcee88fe0abe
4import CarClub from 0xf887ece39166906e
5import Helmet from 0xf887ece39166906e
6import Wheel from 0xf887ece39166906e
7import Car from 0xf887ece39166906e
8import DriverzNFT from 0xa039bd7d55a96c0c
9
10
11access(all) contract DriverzStaking {
12
13    access(all) let StakingStoragePath: StoragePath
14    access(all) let StakingPublicPath: PublicPath
15    access(all) let StakingPrivatePath: PrivatePath
16    access(all) let AdminStoragePath: StoragePath
17
18    // Minimum staking duration (in seconds) before rewards begin accruing.
19    access(all) let minStakeDuration: UFix64
20
21    // Reward rates (Tit Tokens per second):
22    access(all) let rewardRateCarClubCommon: UFix64
23    access(all) let rewardRateCarClubRare: UFix64
24    access(all) let rewardRateCarClubSponsor: UFix64
25    access(all) let rewardRateCarClubLegendary: UFix64 
26    access(all) let rewardRateGenesis: UFix64 
27    access(all) let rewardRateTires: UFix64
28    access(all) let rewardRateHelmets: UFix64
29    access(all) let rewardRateWheels: UFix64
30    access(all) let rewardRateCars: UFix64
31
32    // Treasury vault holding Vroom Tokens used for rewards.
33    access(all) var treasuryVault: @VroomToken.Vault
34
35    // Staked NFT metadata storage
36    access(all) var stakedNFTMetadata: {Address: {UInt64: StakedNFTMetadata}}
37
38    // ✅ FIX: Use `{Address: {UInt64: @{NonFungibleToken.NFT}}}` to store actual NFTs
39    access(all) var stakedNFTAssets: @{Address: {UInt64: {NonFungibleToken.NFT}}}
40
41    // Store reward history for each NFT that was unstaked
42    access(all) var rewardHistory: {Address: {UInt64: UFix64}}
43
44    // Struct to hold staked NFT metadata.
45    access(all) struct StakedNFTMetadata {
46        access(all) let nftID: UInt64
47        access(all) let nftType: String
48        access(all) let nftName: String
49        access(self) var stakeTimestamp: UFix64  // Private to struct, mutable within the struct
50        access(all) var rarity: UInt64
51
52        init(nftID: UInt64, nftType: String, nftName: String, stakeTimestamp: UFix64, rarity: UInt64) {
53            self.nftID = nftID
54            self.nftType = nftType
55            self.nftName = nftName
56            self.stakeTimestamp = stakeTimestamp
57            self.rarity = rarity
58        }
59
60        // Setter function to modify stakeTimestamp
61        access(all) fun updateStakeTimestamp(newTimestamp: UFix64) {
62            self.stakeTimestamp = newTimestamp
63        }
64
65        // ✅ Getter function to access stakeTimestamp
66        access(all) fun getStakeTimestamp(): UFix64 {
67            return self.stakeTimestamp
68        }
69    }
70
71    // ✅ Calculate CarClub Boost Multiplier
72    access(all) fun calcBoost(owner: Address): UFix64 {
73        let account = getAccount(owner)
74
75        let collectionRef = getAccount(owner).capabilities.borrow<&{NonFungibleToken.Collection}>(/public/CarClubCollection)
76            ?? panic("Could not borrow the reference to the CarClub collection")
77
78        let nftCount = collectionRef.getIDs().length
79
80        if nftCount >= 15 {
81            return 2.0
82        } else if nftCount >= 7 {
83            return 1.5
84        }
85        return 1.0
86    }
87
88
89        // Events
90
91    access(all) event NFTStaked(owner: Address, nftID: UInt64, nftType: String, nftName: String, stakeTimestamp: UFix64)
92    access(all) event NFTUnstaked(owner: Address, nftID: UInt64)
93    access(all) event RewardsClaimed(owner: Address, totalReward: UFix64)
94    access(all) event TreasuryFunded(newBalance: UFix64)
95
96    access(all) resource interface DriverzStakePublic {
97        access(all) fun stakeNFT(nft: @{NonFungibleToken.NFT}, nftID: UInt64, nftType: String, nftName: String, rarity: UInt64, owner: Address) 
98        access(all) fun calculateRewards(owner: Address): UFix64
99        access(all) fun getStakedNFTDetails(owner: Address): [StakedNFTMetadata]
100    }
101
102    access(all) resource DriverzStake: DriverzStakePublic {
103
104
105        // 🔒 Secure stake function (No owner parameter)
106        access(all) fun stakeNFT(
107            nft: @{NonFungibleToken.NFT},
108            nftID: UInt64,
109            nftType: String,
110            nftName: String,
111            rarity: UInt64, // rarity is now passed in directly
112            owner: Address
113        ) {
114            let currentTime = getCurrentBlock().timestamp
115
116            // ✅ Store the rarity directly from the argument
117            var metadataDict = DriverzStaking.stakedNFTMetadata[owner] ?? {}
118            metadataDict[nftID] = StakedNFTMetadata(
119                nftID: nftID,
120                nftType: nftType,
121                nftName: nftName,
122                stakeTimestamp: currentTime,
123                rarity: rarity   // Store the rarity here directly
124            )
125            DriverzStaking.stakedNFTMetadata[owner] = metadataDict
126
127            let maybeAssetsDict <- DriverzStaking.stakedNFTAssets.remove(key: owner)
128                ?? panic("No existing staked assets found for this owner.")
129
130            var assetsDict <- maybeAssetsDict
131            assetsDict[nftID] <-! nft
132            DriverzStaking.stakedNFTAssets[owner] <-! assetsDict
133
134            emit NFTStaked(owner: owner, nftID: nftID, nftType: nftType, nftName: nftName, stakeTimestamp: currentTime)
135        }
136
137        access(all) fun unstakeNFT(nftID: UInt64, nftType: String, owner: Address): @{NonFungibleToken.NFT} {
138            let maybeAssetsDict <- DriverzStaking.stakedNFTAssets.remove(key: owner)
139                ?? panic("No staked NFT assets found for owner")
140            var assetsDict <- maybeAssetsDict
141            let nft <- assetsDict.remove(key: nftID)
142                ?? panic("NFT asset not found")
143
144            if assetsDict.length > 0 {
145                DriverzStaking.stakedNFTAssets[owner] <-! assetsDict
146            } else {
147                destroy assetsDict
148            }
149
150            var metadataDict = DriverzStaking.stakedNFTMetadata[owner]
151                ?? panic("No staked NFT metadata found for owner")
152            let nftMetadata = metadataDict[nftID] 
153                ?? panic("NFT metadata not found")
154
155            // Check if the nftType matches
156            if nftMetadata.nftType != nftType {
157                panic("NFT type mismatch during unstaking")
158            }
159
160            // Fetch the CarClub Boost Multiplier
161            let multiplier = DriverzStaking.calcBoost(owner: owner) // ✅ Now using calcBoost
162
163            // Calculate and store the accumulated rewards for this NFT
164            let elapsed = getCurrentBlock().timestamp - nftMetadata.getStakeTimestamp()
165
166            var baseReward: UFix64 = 0.0
167            
168            // ✅ Fixed: Using standard if-else block for baseReward calculation
169            if nftMetadata.nftType == "genesis" {
170                baseReward = elapsed * DriverzStaking.rewardRateGenesis
171            } else if nftMetadata.nftType == "tires" {
172                baseReward = elapsed * DriverzStaking.rewardRateTires
173            } else if nftMetadata.nftType == "wheels" {
174                baseReward = elapsed * DriverzStaking.rewardRateWheels
175            } else if nftMetadata.nftType == "helmets" {
176                baseReward = elapsed * DriverzStaking.rewardRateHelmets
177            } else if nftMetadata.nftType == "cars" {
178                baseReward = elapsed * DriverzStaking.rewardRateCars
179            } else if nftMetadata.nftType == "carclubcommon" {
180                baseReward = elapsed * DriverzStaking.rewardRateCarClubCommon
181            } else if nftMetadata.nftType == "carclubrare" {
182                baseReward = elapsed * DriverzStaking.rewardRateCarClubRare
183            } else if nftMetadata.nftType == "carclubsponsor" {
184                baseReward = elapsed * DriverzStaking.rewardRateCarClubSponsor
185            } else if nftMetadata.nftType == "carclublegendary" {
186                baseReward = elapsed * DriverzStaking.rewardRateCarClubLegendary
187            }
188
189            // ✅ Apply the Boost Multiplier from CarClub NFTs
190            let totalReward = baseReward * multiplier
191
192            // Get the existing reward history for the owner
193            var historyDict = DriverzStaking.rewardHistory[owner] ?? {}
194
195            // Update or add the reward for this specific NFT
196            historyDict[nftID] = (historyDict[nftID] ?? 0.0) + totalReward
197
198            // Save the updated reward history back to the contract state
199            DriverzStaking.rewardHistory[owner] = historyDict
200
201            metadataDict.remove(key: nftID) 
202            DriverzStaking.stakedNFTMetadata[owner] = metadataDict
203
204            emit NFTUnstaked(owner: owner, nftID: nftID)
205            return <- nft
206        }
207
208
209        // 🔒 Secure claim rewards function (Only owner can call)
210        access(all) fun claimRewards(recipient: &{FungibleToken.Receiver}, owner: Address): @VroomToken.Vault {
211            let currentTime = getCurrentBlock().timestamp
212            var totalReward: UFix64 = 0.0
213
214            // Fetch the CarClub Boost Multiplier
215            let boostMultiplier = DriverzStaking.calcBoost(owner: owner)
216
217            // Fetch staked NFT metadata
218            var metadataDict = DriverzStaking.stakedNFTMetadata[owner] 
219                ?? panic("No staked NFT metadata found for owner")
220
221            var updatedMetadataDict: {UInt64: StakedNFTMetadata} = {}
222
223            for nftID in metadataDict.keys {
224                var metadata = metadataDict[nftID]!
225                let elapsed = currentTime - metadata.getStakeTimestamp()
226
227                if elapsed >= DriverzStaking.minStakeDuration {
228                    var baseReward: UFix64 = 0.0
229
230                    // ✅ Update rewards calculation for new NFT types
231                    if metadata.nftType == "genesis" {
232                        baseReward = elapsed * DriverzStaking.rewardRateGenesis
233                    } else if metadata.nftType == "tires" {
234                        baseReward = elapsed * DriverzStaking.rewardRateTires
235                    } else if metadata.nftType == "wheels" {
236                        baseReward = elapsed * DriverzStaking.rewardRateWheels
237                    } else if metadata.nftType == "helmets" {
238                        baseReward = elapsed * DriverzStaking.rewardRateHelmets
239                    } else if metadata.nftType == "cars" {
240                        baseReward = elapsed * DriverzStaking.rewardRateCars
241                    } else if metadata.nftType == "carclubcommon" {
242                        baseReward = elapsed * DriverzStaking.rewardRateCarClubCommon
243                    } else if metadata.nftType == "carclubrare" {
244                        baseReward = elapsed * DriverzStaking.rewardRateCarClubRare
245                    } else if metadata.nftType == "carclubsponsor" {
246                        baseReward = elapsed * DriverzStaking.rewardRateCarClubSponsor
247                    } else if metadata.nftType == "carclublegendary" {
248                        baseReward = elapsed * DriverzStaking.rewardRateCarClubLegendary
249                    }
250
251                    // ✅ Apply the CarClub Boost Multiplier
252                    let totalNFTReward = baseReward * boostMultiplier
253
254                    totalReward = totalReward + totalNFTReward
255
256                    // ✅ ✅ ✅ Update metadata timestamp after claiming
257                    let newMetadata = StakedNFTMetadata(
258                        nftID: metadata.nftID,
259                        nftType: metadata.nftType,
260                        nftName: metadata.nftName,
261                        stakeTimestamp: currentTime,  // 🔥 Reset timestamp after claiming rewards
262                        rarity: metadata.rarity
263                    )
264
265                    updatedMetadataDict[nftID] = newMetadata
266                } else {
267                    updatedMetadataDict[nftID] = metadata
268                }
269            }
270
271            // ✅ Overwrite storage with the updated dictionary
272            DriverzStaking.stakedNFTMetadata[owner] = updatedMetadataDict
273
274            // ✅ Also include rewards from unstaked NFTs
275            let historyDict = DriverzStaking.rewardHistory[owner] ?? {}
276            for reward in historyDict.values {
277                totalReward = totalReward + reward
278            }
279
280            // 🔥 Clear reward history after successfully claiming
281            if totalReward > 0.0 {
282                DriverzStaking.rewardHistory[owner] = {}
283            }
284
285            // Handle the token transfer
286            if totalReward <= 0.0 {
287                panic("No rewards available or minimum staking duration not met")
288            }
289            if DriverzStaking.treasuryVault.balance < totalReward {
290                panic("Insufficient funds in treasury")
291            }
292
293            let rewardVault <- DriverzStaking.treasuryVault.withdraw(amount: totalReward)
294
295            emit RewardsClaimed(owner: owner, totalReward: totalReward)
296            recipient.deposit(from: <- rewardVault)
297
298            return <- VroomToken.createEmptyVault(vaultType: Type<@VroomToken.Vault>())
299        }
300
301
302        // 🔒 Secure reward calculation
303        access(all) fun calculateRewards(owner: Address): UFix64 {
304            let currentTime = getCurrentBlock().timestamp
305            var totalReward: UFix64 = 0.0
306
307            // Fetch the boost multiplier based on CarClub NFTs owned
308            let boostMultiplier = DriverzStaking.calcBoost(owner: owner)
309
310            // Calculate rewards for currently staked NFTs
311            let metadataDict = DriverzStaking.stakedNFTMetadata[owner] 
312                ?? panic("No staked NFT metadata for owner")
313
314            for metadata in metadataDict.values {
315                let elapsed = currentTime - metadata.getStakeTimestamp()
316                if elapsed >= DriverzStaking.minStakeDuration {
317                    var baseReward: UFix64 = 0.0
318
319                    // Assign reward rates based on NFT type
320                    if metadata.nftType == "genesis" {
321                        baseReward = elapsed * DriverzStaking.rewardRateGenesis
322                    } else if metadata.nftType == "tires" {
323                        baseReward = elapsed * DriverzStaking.rewardRateTires
324                    } else if metadata.nftType == "wheels" {
325                        baseReward = elapsed * DriverzStaking.rewardRateWheels
326                    } else if metadata.nftType == "helmets" {
327                        baseReward = elapsed * DriverzStaking.rewardRateHelmets
328                    } else if metadata.nftType == "cars" {
329                        baseReward = elapsed * DriverzStaking.rewardRateCars
330                    } else if metadata.nftType == "carclubcommon" {
331                        baseReward = elapsed * DriverzStaking.rewardRateCarClubCommon
332                    } else if metadata.nftType == "carclubrare" {
333                        baseReward = elapsed * DriverzStaking.rewardRateCarClubRare
334                    } else if metadata.nftType == "carclubsponsor" {
335                        baseReward = elapsed * DriverzStaking.rewardRateCarClubSponsor
336                    } else if metadata.nftType == "carclublegendary" {
337                        baseReward = elapsed * DriverzStaking.rewardRateCarClubLegendary
338                    }
339
340                    // Apply boost multiplier
341                    let totalNFTReward = baseReward * boostMultiplier
342
343                    totalReward = totalReward + totalNFTReward
344                }
345            }
346
347            // ✅ Include rewards from previously unstaked NFTs
348            let historyDict = DriverzStaking.rewardHistory[owner] ?? {}
349            for reward in historyDict.values {
350                totalReward = totalReward + reward
351            }
352
353            return totalReward
354        }
355
356
357
358            // Retrieve details of all staked NFTs for a given owner.
359        access(all) fun getStakedNFTDetails(owner: Address): [StakedNFTMetadata] {
360            let metadataDict = DriverzStaking.stakedNFTMetadata[owner] ?? {}
361            var details: [StakedNFTMetadata] = []
362            for metadata in metadataDict.values {
363                details.append(metadata)
364            }
365            return details
366        }
367
368        access(all) fun createDriverzStaking(): @DriverzStake {
369            return <- create DriverzStaking.DriverzStake()
370        }
371    }
372
373    access(all) fun createDriverzStaking(owner: Address): @DriverzStake {
374        return <- create DriverzStake()
375    }
376
377        // Initialization
378    init(
379
380    ) {
381        self.StakingStoragePath = /storage/driverzStakingStorage
382        self.StakingPublicPath = /public/driverzStakingPublic
383        self.StakingPrivatePath = /private/driverzStakingPrivate
384        self.AdminStoragePath = /storage/driverzStakingAdmin
385
386            // Use the no-argument version of createEmptyVault()
387        self.treasuryVault <- VroomToken.createEmptyVault(vaultType: Type<@VroomToken.Vault>())
388        self.minStakeDuration = 1.0
389        self.rewardRateGenesis = 0.00000724
390        self.rewardRateTires = 0.00000434
391        self.rewardRateHelmets = 0.00000579
392        self.rewardRateWheels = 0.00000868
393        self.rewardRateCars = 0.00002228
394        self.rewardRateCarClubCommon = 0.00002894
395        self.rewardRateCarClubRare = 0.00005788
396        self.rewardRateCarClubSponsor = 0.00008682
397        self.rewardRateCarClubLegendary = 0.00014470
398        self.rewardHistory = {}
399        self.stakedNFTMetadata = {}
400        self.stakedNFTAssets <- {} as @{Address: {UInt64: {NonFungibleToken.NFT}}}
401    }
402
403
404    access(all) fun depositToTreasury(from: @VroomToken.Vault) {
405        self.treasuryVault.deposit(from: <- from)
406        emit TreasuryFunded(newBalance: self.treasuryVault.balance)
407    }
408
409    access(all) fun hasAssets(owner: Address): Bool {
410        return self.stakedNFTAssets[owner] != nil
411    }
412
413
414    access(all) fun createAssetsDictionary(owner: Address) {
415        // Overwrite any existing dictionary by forcefully setting a new empty one
416        self.stakedNFTAssets[owner] <-! {} as @{UInt64: {NonFungibleToken.NFT}}
417    }
418}