Smart Contract
DriverzStaking
A.f887ece39166906e.DriverzStaking
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}