Smart Contract

TitStakingX0X

A.fdfe39186c5e3b90.TitStakingX0X

Deployed

1h ago
Mar 05, 2026, 11:30:11 AM UTC

Dependents

0 imports
1import NonFungibleToken from 0x1d7e57aa55817448
2import TitToken from 0x66b60643244a7738  // Replace with your actual TitToken address
3import FungibleToken from 0xf233dcee88fe0abe
4import PagesX from 0xfdfe39186c5e3b90
5import CourtiersX from 0xfdfe39186c5e3b90
6
7
8access(all) contract TitStakingX0X {
9
10    access(all) let StakingStoragePath: StoragePath
11    access(all) let StakingPublicPath: PublicPath
12    access(all) let StakingPrivatePath: PrivatePath
13    access(all) let AdminStoragePath: StoragePath
14
15    // Minimum staking duration (in seconds) before rewards begin accruing.
16    access(all) let minStakeDuration: UFix64
17
18    // Reward rates (Tit Tokens per second):
19    access(all) let rewardRatePages: UFix64 
20    access(all) let rewardRateCourtiers: UFix64 
21
22    // Treasury vault holding Tit Tokens used for rewards.
23    access(all) var treasuryVault: @TitToken.Vault
24
25    // Staked NFT metadata storage
26    access(all) var stakedNFTMetadata: {Address: {UInt64: StakedNFTMetadata}}
27
28    // ✅ FIX: Use `{Address: {UInt64: @{NonFungibleToken.NFT}}}` to store actual NFTs
29    access(all) var stakedNFTAssets: @{Address: {UInt64: {NonFungibleToken.NFT}}}
30
31    // Store reward history for each NFT that was unstaked
32    access(all) var rewardHistory: {Address: {UInt64: UFix64}}
33
34
35    // Struct to hold staked NFT metadata.
36    access(all) struct StakedNFTMetadata {
37        access(all) let nftID: UInt64
38        access(all) let nftType: String
39        access(all) let nftName: String
40        access(self) var stakeTimestamp: UFix64  // Private to struct, mutable within the struct
41        access(all) var level: UInt64
42
43        init(nftID: UInt64, nftType: String, nftName: String, stakeTimestamp: UFix64, level: UInt64) {
44            self.nftID = nftID
45            self.nftType = nftType
46            self.nftName = nftName
47            self.stakeTimestamp = stakeTimestamp
48            self.level = level
49        }
50
51        // Setter function to modify stakeTimestamp
52        access(all) fun updateStakeTimestamp(newTimestamp: UFix64) {
53            self.stakeTimestamp = newTimestamp
54        }
55
56        // ✅ Getter function to access stakeTimestamp
57        access(all) fun getStakeTimestamp(): UFix64 {
58            return self.stakeTimestamp
59        }
60
61        // Setter function to update the level
62        access(all) fun setLevel(newLevel: UInt64) {
63            self.level = newLevel
64        }
65
66    }
67
68
69    // Events
70
71    access(all) event NFTStaked(owner: Address, nftID: UInt64, nftType: String, nftName: String, stakeTimestamp: UFix64)
72    access(all) event NFTUnstaked(owner: Address, nftID: UInt64)
73    access(all) event RewardsClaimed(owner: Address, totalReward: UFix64)
74    access(all) event TreasuryFunded(newBalance: UFix64)
75
76    access(all) resource interface TitStakePublic {
77        access(all) fun stakeNFT(nft: @{NonFungibleToken.NFT}, nftID: UInt64, nftType: String, nftName: String, level: UInt64, owner: Address) 
78        access(all) fun calculateRewards(owner: Address): UFix64
79        access(all) fun getStakedNFTDetails(owner: Address): [StakedNFTMetadata]
80    }
81
82    // ✅ Private interface for owner-only actions
83  //  access(self) resource interface TitStakePrivate {
84  //      access(all) fun unstakeNFT(nftID: UInt64): @{NonFungibleToken.NFT}
85  //      access(all) fun claimRewards(recipient: &{FungibleToken.Receiver}): @TitToken.Vault
86  //  }
87
88    // ✅ Staking resource with proper access control
89    access(all) resource TitStake: TitStakePublic {
90
91
92
93        // 🔒 Secure stake function (No owner parameter)
94        access(all) fun stakeNFT(
95            nft: @{NonFungibleToken.NFT},
96            nftID: UInt64,
97            nftType: String,
98            nftName: String,
99            level: UInt64, // Level is now passed in directly
100            owner: Address
101        ) {
102            let currentTime = getCurrentBlock().timestamp
103
104            // ✅ Store the level directly from the argument
105            var metadataDict = TitStakingX0X.stakedNFTMetadata[owner] ?? {}
106            metadataDict[nftID] = StakedNFTMetadata(
107                nftID: nftID,
108                nftType: nftType,
109                nftName: nftName,
110                stakeTimestamp: currentTime,
111                level: level   // Store the level here directly
112            )
113            TitStakingX0X.stakedNFTMetadata[owner] = metadataDict
114
115            let maybeAssetsDict <- TitStakingX0X.stakedNFTAssets.remove(key: owner)
116                ?? panic("No existing staked assets found for this owner.")
117
118            var assetsDict <- maybeAssetsDict
119            assetsDict[nftID] <-! nft
120            TitStakingX0X.stakedNFTAssets[owner] <-! assetsDict
121
122            emit NFTStaked(owner: owner, nftID: nftID, nftType: nftType, nftName: nftName, stakeTimestamp: currentTime)
123        }
124
125
126
127        // 🔒 Secure unstake function (Only owner can call)
128
129        //WE NEED TO ADD A SECTION FOR NFT TYPES IN HERE, NOT THAT THEY HAVE THE SAME ID FOR TWO TYPES AND THE CODE DOESN'T KNOW WHO TO REMOVE
130
131
132        access(all) fun unstakeNFT(nftID: UInt64, nftType: String, owner: Address): @{NonFungibleToken.NFT} {
133            let maybeAssetsDict <- TitStakingX0X.stakedNFTAssets.remove(key: owner)
134                ?? panic("No staked NFT assets found for owner")
135            var assetsDict <- maybeAssetsDict
136            let nft <- assetsDict.remove(key: nftID)
137                ?? panic("NFT asset not found")
138
139            if assetsDict.length > 0 {
140                TitStakingX0X.stakedNFTAssets[owner] <-! assetsDict
141            } else {
142                destroy assetsDict
143            }
144
145            var metadataDict = TitStakingX0X.stakedNFTMetadata[owner]
146                ?? panic("No staked NFT metadata found for owner")
147            let nftMetadata = metadataDict[nftID] 
148                ?? panic("NFT metadata not found")
149
150            // Check if the nftType matches
151            if nftMetadata.nftType != nftType {
152                panic("NFT type mismatch during unstaking")
153            }
154
155            // Calculate and store the accumulated rewards for this NFT
156            let elapsed = getCurrentBlock().timestamp - nftMetadata.getStakeTimestamp()
157
158            var baseReward: UFix64 = 0.0
159            
160            // ✅ Fixed: Using standard if-else block for baseReward calculation
161            if nftMetadata.nftType == "pages" {
162                baseReward = elapsed * TitStakingX0X.rewardRatePages
163            } else {
164                baseReward = elapsed * TitStakingX0X.rewardRateCourtiers
165            }
166
167            let multiplier = 1.0 + (UFix64(nftMetadata.level) / 15.0)
168            let totalReward = baseReward * multiplier
169
170            // Get the existing reward history for the owner
171            var historyDict = TitStakingX0X.rewardHistory[owner] ?? {}
172
173            // Update or add the reward for this specific NFT
174            historyDict[nftID] = (historyDict[nftID] ?? 0.0) + totalReward
175
176            // Save the updated reward history back to the contract state
177            TitStakingX0X.rewardHistory[owner] = historyDict
178
179
180            metadataDict.remove(key: nftID) 
181            TitStakingX0X.stakedNFTMetadata[owner] = metadataDict
182
183            emit NFTUnstaked(owner: owner, nftID: nftID)
184            return <- nft
185        }
186
187        // 🔒 Secure claim rewards function (Only owner can call)
188        access(all) fun claimRewards(recipient: &{FungibleToken.Receiver}, owner: Address): @TitToken.Vault {
189            let currentTime = getCurrentBlock().timestamp
190            var totalReward: UFix64 = 0.0
191
192            // Fetch staked NFT metadata
193            var metadataDict = TitStakingX0X.stakedNFTMetadata[owner] 
194                ?? panic("No staked NFT metadata found for owner")
195
196            var updatedMetadataDict: {UInt64: StakedNFTMetadata} = {}
197
198            for nftID in metadataDict.keys {
199                var metadata = metadataDict[nftID]!
200                let elapsed = currentTime - metadata.getStakeTimestamp()
201
202                if elapsed >= TitStakingX0X.minStakeDuration {
203                    var baseReward: UFix64 = 0.0
204                    if metadata.nftType == "pages" {
205                        baseReward = elapsed * TitStakingX0X.rewardRatePages
206                    } else if metadata.nftType == "courtiers" {
207                        baseReward = elapsed * TitStakingX0X.rewardRateCourtiers
208                    }
209                    let multiplier = 1.0 + (UFix64(metadata.level) / 15.0)
210                    totalReward = totalReward + (baseReward * multiplier)
211
212                    // ✅ ✅ ✅ Recreate the struct and reinsert it into the dictionary
213                    let newMetadata = StakedNFTMetadata(
214                        nftID: metadata.nftID,
215                        nftType: metadata.nftType,
216                        nftName: metadata.nftName,
217                        stakeTimestamp: currentTime,  // 🔥 SET NEW TIMESTAMP
218                        level: metadata.level
219                    )
220
221                    updatedMetadataDict[nftID] = newMetadata
222                } else {
223                    updatedMetadataDict[nftID] = metadata
224                }
225            }
226
227            // ✅ Overwrite storage with the updated dictionary
228            TitStakingX0X.stakedNFTMetadata[owner] = updatedMetadataDict
229
230            // ✅ Also include rewards from unstaked NFTs
231            let historyDict = TitStakingX0X.rewardHistory[owner] ?? {}
232            for reward in historyDict.values {
233                totalReward = totalReward + reward
234            }
235
236            // 🔥 Clear reward history after successfully claiming
237            if totalReward > 0.0 {
238                TitStakingX0X.rewardHistory[owner] = {}
239            }
240
241            // Handle the token transfer
242            if totalReward <= 0.0 {
243                panic("No rewards available or minimum staking duration not met")
244            }
245            if TitStakingX0X.treasuryVault.balance < totalReward {
246                panic("Insufficient funds in treasury")
247            }
248
249            let rewardVault <- TitStakingX0X.treasuryVault.withdraw(amount: totalReward)
250
251            emit RewardsClaimed(owner: owner, totalReward: totalReward)
252            recipient.deposit(from: <- rewardVault)
253
254            return <- TitToken.createEmptyVault(vaultType: Type<@TitToken.Vault>())
255        }
256
257
258        // 🔒 Secure reward calculation
259        access(all) fun calculateRewards(owner: Address): UFix64 {
260            let currentTime = getCurrentBlock().timestamp
261            var totalReward: UFix64 = 0.0
262
263            // Calculate rewards for currently staked NFTs
264            let metadataDict = TitStakingX0X.stakedNFTMetadata[owner] 
265                ?? panic("No staked NFT metadata for owner")
266
267            for metadata in metadataDict.values {
268                let elapsed = currentTime - metadata.getStakeTimestamp()
269                if elapsed >= TitStakingX0X.minStakeDuration {
270                    var baseReward: UFix64 = 0.0
271                    if metadata.nftType == "pages" {
272                        baseReward = elapsed * TitStakingX0X.rewardRatePages
273                    } else if metadata.nftType == "courtiers" {
274                        baseReward = elapsed * TitStakingX0X.rewardRateCourtiers
275                    }
276                    // Compute multiplier: ranges linearly from 1 (level 0) to 2 (level 15)
277                    let multiplier = 1.0 + (UFix64(metadata.level) / 15.0)
278                    totalReward = totalReward + (baseReward * multiplier)
279                }
280            }
281
282            // ✅ Include rewards from previously unstaked NFTs
283            let historyDict = TitStakingX0X.rewardHistory[owner] ?? {}
284            for reward in historyDict.values {
285                totalReward = totalReward + reward
286            }
287
288            return totalReward
289        }
290
291
292            // Retrieve details of all staked NFTs for a given owner.
293        access(all) fun getStakedNFTDetails(owner: Address): [StakedNFTMetadata] {
294            let metadataDict = TitStakingX0X.stakedNFTMetadata[owner] ?? {}
295            var details: [StakedNFTMetadata] = []
296            for metadata in metadataDict.values {
297                details.append(metadata)
298            }
299            return details
300        }
301
302        access(all) fun createTitStaking(): @TitStake {
303            return <- create TitStakingX0X.TitStake()
304        }
305
306
307
308    }
309
310
311        access(all) fun createTitStaking(owner: Address): @TitStake {
312            return <- create TitStake()
313        }
314
315
316
317    // Initialization
318    init(
319
320    ) {
321        self.StakingStoragePath = /storage/titStakingXStorage
322        self.StakingPublicPath = /public/titStakingXPublic
323        self.StakingPrivatePath = /private/titStakingXPrivate
324        self.AdminStoragePath = /storage/titStakingXAdmin
325
326            // Use the no-argument version of createEmptyVault()
327        self.treasuryVault <- TitToken.createEmptyVault(vaultType: Type<@TitToken.Vault>())
328        self.minStakeDuration = 1.0
329        self.rewardRatePages = 0.00000579
330        self.rewardRateCourtiers = 0.00001158
331        self.rewardHistory = {}
332        self.stakedNFTMetadata = {}
333        self.stakedNFTAssets <- {} as @{Address: {UInt64: {NonFungibleToken.NFT}}}
334    }
335
336
337    access(all) fun depositToTreasury(from: @TitToken.Vault) {
338        self.treasuryVault.deposit(from: <- from)
339        emit TreasuryFunded(newBalance: self.treasuryVault.balance)
340    }
341
342    access(all) fun hasAssets(owner: Address): Bool {
343        return self.stakedNFTAssets[owner] != nil
344    }
345
346
347    access(all) fun createAssetsDictionary(owner: Address) {
348        // Overwrite any existing dictionary by forcefully setting a new empty one
349        self.stakedNFTAssets[owner] <-! {} as @{UInt64: {NonFungibleToken.NFT}}
350    }
351
352
353
354}
355