Smart Contract
DriverzStakingX
A.f887ece39166906e.DriverzStakingX
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 DriverzStakingX {
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: UFix64
51
52 init(nftID: UInt64, nftType: String, nftName: String, stakeTimestamp: UFix64, rarity: UFix64) {
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, 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 owner: Address
112 ) {
113 let currentTime = getCurrentBlock().timestamp
114
115 // ✅ Get Boost Multiplier from calcBoost function
116 let boostMultiplier = DriverzStakingX.calcBoost(owner: owner)
117
118 // ✅ Store the rarity directly from the argument
119 var metadataDict = DriverzStakingX.stakedNFTMetadata[owner] ?? {}
120 metadataDict[nftID] = StakedNFTMetadata(
121 nftID: nftID,
122 nftType: nftType,
123 nftName: nftName,
124 stakeTimestamp: currentTime,
125 rarity: boostMultiplier // Store the rarity here directly
126 )
127 DriverzStakingX.stakedNFTMetadata[owner] = metadataDict
128
129 let maybeAssetsDict <- DriverzStakingX.stakedNFTAssets.remove(key: owner)
130 ?? panic("No existing staked assets found for this owner.")
131
132 var assetsDict <- maybeAssetsDict
133 assetsDict[nftID] <-! nft
134 DriverzStakingX.stakedNFTAssets[owner] <-! assetsDict
135
136 emit NFTStaked(owner: owner, nftID: nftID, nftType: nftType, nftName: nftName, stakeTimestamp: currentTime)
137 }
138
139 access(all) fun unstakeNFT(nftID: UInt64, nftType: String, owner: Address): @{NonFungibleToken.NFT} {
140 let maybeAssetsDict <- DriverzStakingX.stakedNFTAssets.remove(key: owner)
141 ?? panic("No staked NFT assets found for owner")
142 var assetsDict <- maybeAssetsDict
143 let nft <- assetsDict.remove(key: nftID)
144 ?? panic("NFT asset not found")
145
146 if assetsDict.length > 0 {
147 DriverzStakingX.stakedNFTAssets[owner] <-! assetsDict
148 } else {
149 destroy assetsDict
150 }
151
152 var metadataDict = DriverzStakingX.stakedNFTMetadata[owner]
153 ?? panic("No staked NFT metadata found for owner")
154 let nftMetadata = metadataDict[nftID]
155 ?? panic("NFT metadata not found")
156
157 // Check if the nftType matches
158 if nftMetadata.nftType != nftType {
159 panic("NFT type mismatch during unstaking")
160 }
161
162 // Fetch the CarClub Boost Multiplier
163 let multiplier = DriverzStakingX.calcBoost(owner: owner) // ✅ Now using calcBoost
164
165 // Calculate and store the accumulated rewards for this NFT
166 let elapsed = getCurrentBlock().timestamp - nftMetadata.getStakeTimestamp()
167
168 var baseReward: UFix64 = 0.0
169
170 // ✅ Fixed: Using standard if-else block for baseReward calculation
171 if nftMetadata.nftType == "genesis" {
172 baseReward = elapsed * DriverzStakingX.rewardRateGenesis
173 } else if nftMetadata.nftType == "tires" {
174 baseReward = elapsed * DriverzStakingX.rewardRateTires
175 } else if nftMetadata.nftType == "wheels" {
176 baseReward = elapsed * DriverzStakingX.rewardRateWheels
177 } else if nftMetadata.nftType == "helmets" {
178 baseReward = elapsed * DriverzStakingX.rewardRateHelmets
179 } else if nftMetadata.nftType == "cars" {
180 baseReward = elapsed * DriverzStakingX.rewardRateCars
181 } else if nftMetadata.nftType == "carclubcommon" {
182 baseReward = elapsed * DriverzStakingX.rewardRateCarClubCommon
183 } else if nftMetadata.nftType == "carclubrare" {
184 baseReward = elapsed * DriverzStakingX.rewardRateCarClubRare
185 } else if nftMetadata.nftType == "carclubsponsor" {
186 baseReward = elapsed * DriverzStakingX.rewardRateCarClubSponsor
187 } else if nftMetadata.nftType == "carclublegendary" {
188 baseReward = elapsed * DriverzStakingX.rewardRateCarClubLegendary
189 }
190
191 // ✅ Apply the Boost Multiplier from CarClub NFTs
192 let totalReward = baseReward * multiplier
193
194 // Get the existing reward history for the owner
195 var historyDict = DriverzStakingX.rewardHistory[owner] ?? {}
196
197 // Update or add the reward for this specific NFT
198 historyDict[nftID] = (historyDict[nftID] ?? 0.0) + totalReward
199
200 // Save the updated reward history back to the contract state
201 DriverzStakingX.rewardHistory[owner] = historyDict
202
203 metadataDict.remove(key: nftID)
204 DriverzStakingX.stakedNFTMetadata[owner] = metadataDict
205
206 emit NFTUnstaked(owner: owner, nftID: nftID)
207 return <- nft
208 }
209
210
211 // 🔒 Secure claim rewards function (Only owner can call)
212 access(all) fun claimRewards(recipient: &{FungibleToken.Receiver}, owner: Address): @VroomToken.Vault {
213 let currentTime = getCurrentBlock().timestamp
214 var totalReward: UFix64 = 0.0
215
216 // Fetch the CarClub Boost Multiplier
217 let boostMultiplier = DriverzStakingX.calcBoost(owner: owner)
218
219 // Fetch staked NFT metadata
220 var metadataDict = DriverzStakingX.stakedNFTMetadata[owner]
221 ?? panic("No staked NFT metadata found for owner")
222
223 var updatedMetadataDict: {UInt64: StakedNFTMetadata} = {}
224
225 for nftID in metadataDict.keys {
226 var metadata = metadataDict[nftID]!
227 let elapsed = currentTime - metadata.getStakeTimestamp()
228
229 if elapsed >= DriverzStakingX.minStakeDuration {
230 var baseReward: UFix64 = 0.0
231
232 // ✅ Update rewards calculation for new NFT types
233 if metadata.nftType == "genesis" {
234 baseReward = elapsed * DriverzStakingX.rewardRateGenesis
235 } else if metadata.nftType == "tires" {
236 baseReward = elapsed * DriverzStakingX.rewardRateTires
237 } else if metadata.nftType == "wheels" {
238 baseReward = elapsed * DriverzStakingX.rewardRateWheels
239 } else if metadata.nftType == "helmets" {
240 baseReward = elapsed * DriverzStakingX.rewardRateHelmets
241 } else if metadata.nftType == "cars" {
242 baseReward = elapsed * DriverzStakingX.rewardRateCars
243 } else if metadata.nftType == "carclubcommon" {
244 baseReward = elapsed * DriverzStakingX.rewardRateCarClubCommon
245 } else if metadata.nftType == "carclubrare" {
246 baseReward = elapsed * DriverzStakingX.rewardRateCarClubRare
247 } else if metadata.nftType == "carclubsponsor" {
248 baseReward = elapsed * DriverzStakingX.rewardRateCarClubSponsor
249 } else if metadata.nftType == "carclublegendary" {
250 baseReward = elapsed * DriverzStakingX.rewardRateCarClubLegendary
251 }
252
253 // ✅ Apply the CarClub Boost Multiplier
254 let totalNFTReward = baseReward * boostMultiplier
255
256 totalReward = totalReward + totalNFTReward
257
258 // ✅ ✅ ✅ Update metadata timestamp after claiming
259 let newMetadata = StakedNFTMetadata(
260 nftID: metadata.nftID,
261 nftType: metadata.nftType,
262 nftName: metadata.nftName,
263 stakeTimestamp: currentTime, // 🔥 Reset timestamp after claiming rewards
264 rarity: metadata.rarity
265 )
266
267 updatedMetadataDict[nftID] = newMetadata
268 } else {
269 updatedMetadataDict[nftID] = metadata
270 }
271 }
272
273 // ✅ Overwrite storage with the updated dictionary
274 DriverzStakingX.stakedNFTMetadata[owner] = updatedMetadataDict
275
276 // ✅ Also include rewards from unstaked NFTs
277 let historyDict = DriverzStakingX.rewardHistory[owner] ?? {}
278 for reward in historyDict.values {
279 totalReward = totalReward + reward
280 }
281
282 // 🔥 Clear reward history after successfully claiming
283 if totalReward > 0.0 {
284 DriverzStakingX.rewardHistory[owner] = {}
285 }
286
287 // Handle the token transfer
288 if totalReward <= 0.0 {
289 panic("No rewards available or minimum staking duration not met")
290 }
291 if DriverzStakingX.treasuryVault.balance < totalReward {
292 panic("Insufficient funds in treasury")
293 }
294
295 let rewardVault <- DriverzStakingX.treasuryVault.withdraw(amount: totalReward)
296
297 emit RewardsClaimed(owner: owner, totalReward: totalReward)
298 recipient.deposit(from: <- rewardVault)
299
300 return <- VroomToken.createEmptyVault(vaultType: Type<@VroomToken.Vault>())
301 }
302
303
304 // 🔒 Secure reward calculation
305 access(all) fun calculateRewards(owner: Address): UFix64 {
306 let currentTime = getCurrentBlock().timestamp
307 var totalReward: UFix64 = 0.0
308
309 // Fetch the boost multiplier based on CarClub NFTs owned
310 let boostMultiplier = DriverzStakingX.calcBoost(owner: owner)
311
312 // Calculate rewards for currently staked NFTs
313 let metadataDict = DriverzStakingX.stakedNFTMetadata[owner]
314 ?? panic("No staked NFT metadata for owner")
315
316 for metadata in metadataDict.values {
317 let elapsed = currentTime - metadata.getStakeTimestamp()
318 if elapsed >= DriverzStakingX.minStakeDuration {
319 var baseReward: UFix64 = 0.0
320
321 // Assign reward rates based on NFT type
322 if metadata.nftType == "genesis" {
323 baseReward = elapsed * DriverzStakingX.rewardRateGenesis
324 } else if metadata.nftType == "tires" {
325 baseReward = elapsed * DriverzStakingX.rewardRateTires
326 } else if metadata.nftType == "wheels" {
327 baseReward = elapsed * DriverzStakingX.rewardRateWheels
328 } else if metadata.nftType == "helmets" {
329 baseReward = elapsed * DriverzStakingX.rewardRateHelmets
330 } else if metadata.nftType == "cars" {
331 baseReward = elapsed * DriverzStakingX.rewardRateCars
332 } else if metadata.nftType == "carclubcommon" {
333 baseReward = elapsed * DriverzStakingX.rewardRateCarClubCommon
334 } else if metadata.nftType == "carclubrare" {
335 baseReward = elapsed * DriverzStakingX.rewardRateCarClubRare
336 } else if metadata.nftType == "carclubsponsor" {
337 baseReward = elapsed * DriverzStakingX.rewardRateCarClubSponsor
338 } else if metadata.nftType == "carclublegendary" {
339 baseReward = elapsed * DriverzStakingX.rewardRateCarClubLegendary
340 }
341
342 // Apply boost multiplier
343 let totalNFTReward = baseReward * boostMultiplier
344
345 totalReward = totalReward + totalNFTReward
346 }
347 }
348
349 // ✅ Include rewards from previously unstaked NFTs
350 let historyDict = DriverzStakingX.rewardHistory[owner] ?? {}
351 for reward in historyDict.values {
352 totalReward = totalReward + reward
353 }
354
355 return totalReward
356 }
357
358
359
360 // Retrieve details of all staked NFTs for a given owner.
361 access(all) fun getStakedNFTDetails(owner: Address): [StakedNFTMetadata] {
362 let metadataDict = DriverzStakingX.stakedNFTMetadata[owner] ?? {}
363 var details: [StakedNFTMetadata] = []
364 for metadata in metadataDict.values {
365 details.append(metadata)
366 }
367 return details
368 }
369
370 access(all) fun createDriverzStakingX(): @DriverzStake {
371 return <- create DriverzStakingX.DriverzStake()
372 }
373 }
374
375 access(all) fun createDriverzStakingX(owner: Address): @DriverzStake {
376 return <- create DriverzStake()
377 }
378
379 // Initialization
380 init(
381
382 ) {
383 self.StakingStoragePath = /storage/driverzStakingStorage
384 self.StakingPublicPath = /public/driverzStakingPublic
385 self.StakingPrivatePath = /private/driverzStakingPrivate
386 self.AdminStoragePath = /storage/driverzStakingAdmin
387
388 // Use the no-argument version of createEmptyVault()
389 self.treasuryVault <- VroomToken.createEmptyVault(vaultType: Type<@VroomToken.Vault>())
390 self.minStakeDuration = 1.0
391 self.rewardRateGenesis = 0.00000724
392 self.rewardRateTires = 0.00000434
393 self.rewardRateHelmets = 0.00000579
394 self.rewardRateWheels = 0.00000868
395 self.rewardRateCars = 0.00002228
396 self.rewardRateCarClubCommon = 0.00002894
397 self.rewardRateCarClubRare = 0.00005788
398 self.rewardRateCarClubSponsor = 0.00008682
399 self.rewardRateCarClubLegendary = 0.00014470
400 self.rewardHistory = {}
401 self.stakedNFTMetadata = {}
402 self.stakedNFTAssets <- {} as @{Address: {UInt64: {NonFungibleToken.NFT}}}
403 }
404
405
406 access(all) fun depositToTreasury(from: @VroomToken.Vault) {
407 self.treasuryVault.deposit(from: <- from)
408 emit TreasuryFunded(newBalance: self.treasuryVault.balance)
409 }
410
411 access(all) fun hasAssets(owner: Address): Bool {
412 return self.stakedNFTAssets[owner] != nil
413 }
414
415
416 access(all) fun createAssetsDictionary(owner: Address) {
417 // Overwrite any existing dictionary by forcefully setting a new empty one
418 self.stakedNFTAssets[owner] <-! {} as @{UInt64: {NonFungibleToken.NFT}}
419 }
420}