Smart Contract

BloctoPass

A.0f9df91c9121c460.BloctoPass

Deployed

1w ago
Feb 15, 2026, 04:38:48 PM UTC

Dependents

16 imports
1// This is the implementation of BloctoPass, the Blocto Non-Fungible Token
2// that is used in-conjunction with BLT, the Blocto Fungible Token
3
4import FungibleToken from 0xf233dcee88fe0abe 
5import NonFungibleToken from 0x1d7e57aa55817448
6import BloctoToken from 0x0f9df91c9121c460
7import BloctoTokenStaking from 0x0f9df91c9121c460 
8import BloctoPassStamp from 0x0f9df91c9121c460
9
10import ViewResolver from 0x1d7e57aa55817448
11import MetadataViews from 0x1d7e57aa55817448
12
13access(all)
14contract BloctoPass: NonFungibleToken {
15    // An entitlement for BloctoPassPrivate access
16    access(all) entitlement BloctoPassPrivateEntitlement
17    // An entitlement for CollectionPrivate access
18    access(all) entitlement CollectionPrivateEntitlement
19    // An entitlement for MinterPublic access
20    access(all) entitlement MinterPublicEntitlement
21    // An entitlement for NFTMinter access
22    access(all) entitlement NFTMinterEntitlement
23
24    access(all)
25    var totalSupply: UInt64
26    
27    access(all)
28    let CollectionStoragePath: StoragePath
29    
30    access(all)
31    let CollectionPublicPath: PublicPath
32    
33    access(all)
34    let MinterStoragePath: StoragePath
35    
36    access(all)
37    let MinterPublicPath: PublicPath
38    
39    // pre-defined lockup schedules
40    // key: timestamp
41    // value: percentage of BLT that must remain in the BloctoPass at this timestamp
42    access(contract)
43    var predefinedLockupSchedules: [{UFix64: UFix64}]
44    
45    access(all)
46    event ContractInitialized()
47    
48    access(all)
49    event Withdraw(id: UInt64, from: Address?)
50    
51    access(all)
52    event Deposit(id: UInt64, to: Address?)
53    
54    access(all)
55    event LockupScheduleDefined(id: Int, lockupSchedule: {UFix64: UFix64})
56    
57    access(all)
58    event LockupScheduleUpdated(id: Int, lockupSchedule: {UFix64: UFix64})
59    
60    access(all)
61    resource interface BloctoPassPrivate {
62        access(BloctoPassPrivateEntitlement)
63        fun stakeNewTokens(amount: UFix64): Void
64        
65        access(BloctoPassPrivateEntitlement)
66        fun stakeUnstakedTokens(amount: UFix64)
67        
68        access(BloctoPassPrivateEntitlement)
69        fun stakeRewardedTokens(amount: UFix64)
70        
71        access(BloctoPassPrivateEntitlement)
72        fun requestUnstaking(amount: UFix64)
73        
74        access(BloctoPassPrivateEntitlement)
75        fun unstakeAll()
76        
77        access(BloctoPassPrivateEntitlement)
78        fun withdrawUnstakedTokens(amount: UFix64)
79        
80        access(BloctoPassPrivateEntitlement)
81        fun withdrawRewardedTokens(amount: UFix64)
82        
83        access(BloctoPassPrivateEntitlement)
84        fun withdrawAllUnlockedTokens(): @{FungibleToken.Vault}
85        
86        access(BloctoPassPrivateEntitlement)
87        fun stampBloctoPass(from: @BloctoPassStamp.NFT)
88    }
89    
90    access(all)
91    resource interface BloctoPassPublic {
92        access(all)
93        fun getOriginalOwner(): Address?
94        
95        access(all)
96        fun getMetadata(): {String: String}
97        
98        access(all)
99        fun getStamps(): [String]
100        
101        access(all)
102        fun getVipTier(): UInt64
103        
104        access(all)
105        fun getStakingInfo(): BloctoTokenStaking.StakerInfo
106        
107        access(all)
108        fun getLockupSchedule(): {UFix64: UFix64}
109        
110        access(all)
111        fun getLockupAmountAtTimestamp(timestamp: UFix64): UFix64
112        
113        access(all)
114        fun getLockupAmount(): UFix64
115        
116        access(all)
117        fun getIdleBalance(): UFix64
118
119        access(all)
120        fun getTotalBalance(): UFix64
121    }
122    
123    access(all)
124    resource NFT:
125        NonFungibleToken.NFT,
126        FungibleToken.Provider,
127        FungibleToken.Receiver,
128        BloctoPassPrivate,
129        BloctoPassPublic
130    {
131        // BLT holder vault
132        access(self)
133        let vault: @BloctoToken.Vault
134
135        // BLT staker handle
136        access(self)
137        let staker: @BloctoTokenStaking.Staker
138
139        // BloctoPass ID
140        access(all)
141        let id: UInt64
142
143        // BloctoPass owner address
144        // If the pass is transferred to another user, some perks will be disabled
145        access(all)
146        let originalOwner: Address?
147
148        // BloctoPass metadata
149        access(self)
150        var metadata: {String: String}
151
152        // BloctoPass usage stamps, including voting records and special events
153        access(self)
154        var stamps: [String]
155
156        // Total amount that's subject to lockup schedule
157        access(all)
158        let lockupAmount: UFix64
159
160        // ID of predefined lockup schedule
161        // If lockupScheduleId == nil, use custom lockup schedule instead
162        access(all)
163        let lockupScheduleId: Int?
164
165        // Defines how much BloctoToken must remain in the BloctoPass on different dates
166        // key: timestamp
167        // value: percentage of BLT that must remain in the BloctoPass at this timestamp
168        access(self)
169        let lockupSchedule: {UFix64: UFix64}?
170
171        init(
172            initID: UInt64,
173            originalOwner: Address?,
174            metadata: {String: String},
175            vault: @{FungibleToken.Vault},
176            lockupScheduleId: Int?,
177            lockupSchedule:{UFix64: UFix64}?
178        ) {
179            let stakingAdmin = BloctoPass.account.storage.borrow<auth(BloctoTokenStaking.AdminEntitlement) &BloctoTokenStaking.Admin>(from: BloctoTokenStaking.StakingAdminStoragePath) 
180                ?? panic("Could not borrow admin reference")
181
182            self.id = initID
183            self.originalOwner = originalOwner
184            self.metadata = metadata
185            self.stamps = []
186            self.vault <- vault as! @BloctoToken.Vault
187            self.staker <- stakingAdmin.addStakerRecord(id: initID)
188
189            // lockup calculations
190            self.lockupAmount = self.vault.balance
191            self.lockupScheduleId = lockupScheduleId
192            self.lockupSchedule = lockupSchedule
193        }
194
195        access(FungibleToken.Withdraw)
196        fun withdraw(amount: UFix64): @{FungibleToken.Vault} {
197            post {
198                self.getTotalBalance() >= self.getLockupAmount(): "Cannot withdraw locked-up BLTs"
199            }
200
201            return <- self.vault.withdraw(amount: amount)
202        }
203
204        access(all)
205        fun deposit(from: @{FungibleToken.Vault}): Void {
206            self.vault.deposit(from: <-from)
207        }
208
209        access(all)
210        fun getOriginalOwner(): Address? {
211            return self.originalOwner
212        }
213
214        access(all)
215        fun getMetadata(): {String: String} {
216            return self.metadata
217        }
218
219        access(all)
220        fun getStamps(): [String] {
221            return self.stamps
222        }
223
224        access(all)
225        fun getVipTier(): UInt64 {
226            // Disable VIP tier at launch
227            // let stakedAmount = self.getStakingInfo().tokensStaked
228            // if stakedAmount >= 1000.0 {
229            //     return 1
230            // }
231            
232            // TODO: add more tiers
233
234            return 0
235        }
236
237        access(all)
238        view fun getLockupSchedule(): {UFix64: UFix64} {
239            if self.lockupScheduleId == nil {
240                return self.lockupSchedule ?? {0.0: 0.0}
241            }
242            return BloctoPass.predefinedLockupSchedules[self.lockupScheduleId!]
243        }
244
245        access(all)
246        fun getStakingInfo(): BloctoTokenStaking.StakerInfo {
247            return BloctoTokenStaking.StakerInfo(stakerID: self.id)
248        }
249
250        access(all)
251        view fun getLockupAmountAtTimestamp(timestamp: UFix64): UFix64 {
252            if (self.lockupAmount == 0.0) {
253                return 0.0
254            }
255
256            let lockupSchedule = self.getLockupSchedule()
257
258            let keys = lockupSchedule.keys
259            var closestTimestamp = 0.0
260            var lockupPercentage = 0.0
261
262            for key in keys {
263                if timestamp >= key && key >= closestTimestamp {
264                    lockupPercentage = lockupSchedule[key]!
265                    closestTimestamp = key
266                }
267            }
268            return lockupPercentage * self.lockupAmount
269        }
270
271        access(all)
272        view fun getLockupAmount(): UFix64 {
273            return self.getLockupAmountAtTimestamp(timestamp: getCurrentBlock().timestamp)
274        }
275
276        access(all)
277        view fun getIdleBalance(): UFix64 {
278            return self.vault.balance
279        }
280
281        access(all)
282        view fun getTotalBalance(): UFix64 {
283            return self.getIdleBalance() + BloctoTokenStaking.StakerInfo(stakerID: self.id).totalTokensInRecord()
284        }
285
286        // Private staking methods
287        access(BloctoPassPrivateEntitlement)
288        fun stakeNewTokens(amount: UFix64) {
289            self.staker.stakeNewTokens(<-self.vault.withdraw(amount: amount))
290        }
291
292        access(BloctoPassPrivateEntitlement)
293        fun stakeUnstakedTokens(amount: UFix64) {
294            self.staker.stakeUnstakedTokens(amount: amount)
295        }
296
297        access(BloctoPassPrivateEntitlement)
298        fun stakeRewardedTokens(amount: UFix64) {
299            self.staker.stakeRewardedTokens(amount: amount)
300        }
301
302        access(BloctoPassPrivateEntitlement)
303        fun requestUnstaking(amount: UFix64) {
304            self.staker.requestUnstaking(amount: amount)
305        }
306        
307        access(BloctoPassPrivateEntitlement)
308        fun unstakeAll() {
309            self.staker.unstakeAll()
310        }
311        
312        access(BloctoPassPrivateEntitlement)
313        fun withdrawUnstakedTokens(amount: UFix64) {
314            let vault <- self.staker.withdrawUnstakedTokens(amount: amount)
315            self.vault.deposit(from: <-vault)
316        }
317
318        access(BloctoPassPrivateEntitlement)
319        fun withdrawRewardedTokens(amount: UFix64) {
320            let vault <- self.staker.withdrawRewardedTokens(amount: amount)
321            self.vault.deposit(from: <-vault)
322        }
323
324        access(BloctoPassPrivateEntitlement)
325        fun withdrawAllUnlockedTokens(): @{FungibleToken.Vault} {
326            let unlockedAmount = self.getTotalBalance() - self.getLockupAmount()
327            let withdrawAmount = unlockedAmount < self.getIdleBalance() ? unlockedAmount : self.getIdleBalance()
328            return <- self.vault.withdraw(amount: withdrawAmount)
329        }
330
331        access(BloctoPassPrivateEntitlement)
332        fun stampBloctoPass(from: @BloctoPassStamp.NFT) {
333            self.stamps.append(from.getMessage())
334            destroy from
335        }
336
337        access(all)
338        fun createEmptyCollection(): @{NonFungibleToken.Collection} {
339            return <-create Collection()
340        }
341        
342        access(all)
343        view fun isAvailableToWithdraw(amount: UFix64): Bool {
344            return false
345        }
346
347        access(all) view fun getViews(): [Type] {
348            return [
349                Type<MetadataViews.Display>()
350            ]
351        }
352
353        access(all) fun resolveView(_ view: Type): AnyStruct? {
354            switch view {
355                case Type<MetadataViews.Display>():
356                    return MetadataViews.Display(
357                        name: "Blocto Pass",
358                        description: "",
359                        thumbnail: MetadataViews.HTTPFile(
360                            url: "https://raw.githubusercontent.com/portto/assets-v2/master/nft/blocto-pass/logo.png"
361                        )
362                    )
363            }
364            return nil
365        }
366
367        // getSupportedVaultTypes optionally returns a list of vault types that this receiver accepts
368        access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
369            return {self.getType(): true}
370        }
371
372        access(all) view fun isSupportedVaultType(type: Type): Bool {
373            if (type == self.getType())  {return true } else  {return false }
374        }
375    }
376
377    // CollectionPublic is a custom interface that allows us to
378    // access the public fields and methods for our BloctoPass Collection
379    access(all)
380    resource interface CollectionPublic {
381        access(all)
382        fun borrowBloctoPassPublic(id: UInt64): &BloctoPass.NFT
383    }
384    
385    access(all)
386    resource interface CollectionPrivate {
387        access(CollectionPrivateEntitlement)
388        fun borrowBloctoPassPrivate(id: UInt64): auth(BloctoPass.BloctoPassPrivateEntitlement) &BloctoPass.NFT
389
390        access(CollectionPrivateEntitlement)
391        fun borrowWithdraw(id: UInt64): auth(FungibleToken.Withdraw) &BloctoPass.NFT
392    }
393    
394    access(all)
395    resource Collection:  
396        NonFungibleToken.Collection,
397        CollectionPublic,
398        CollectionPrivate
399    {
400        // dictionary of NFT conforming tokens
401        // NFT is a resource type with an `UInt64` ID field
402        access(all)
403        var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
404
405        init () {
406            self.ownedNFTs <- {}
407        }
408
409        // withdraw removes an NFT from the collection and moves it to the caller
410        // withdrawal is disabled during lockup period
411        access(NonFungibleToken.Withdraw)
412        fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
413            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
414            emit Withdraw(id: token.id, from: self.owner?.address)
415            return <-token
416        }
417
418        // deposit takes a NFT and adds it to the collections dictionary
419        // and adds the ID to the id array
420        access(all)
421        fun deposit(token: @{NonFungibleToken.NFT}): Void {
422            let token <- token as! @BloctoPass.NFT
423            let id: UInt64 = token.id
424
425            // add the new token to the dictionary which removes the old one
426            let oldToken <- self.ownedNFTs[id] <- token
427
428            emit Deposit(id: id, to: self.owner?.address)
429
430            destroy oldToken
431        }
432
433        // getIDs returns an array of the IDs that are in the collection
434        access(all)
435        view fun getIDs(): [UInt64] {
436            return self.ownedNFTs.keys
437        }
438
439        // borrowNFT gets a reference to an NFT in the collection
440        // so that the caller can read its metadata and call its methods
441        access(all)
442        view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
443            return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
444        }
445
446        // borrowBloctoPassPublic gets the public references to a BloctoPass NFT in the collection
447        // and returns it to the caller as a reference to the NFT
448        access(all)
449        fun borrowBloctoPassPublic(id: UInt64): &BloctoPass.NFT {
450            let bloctoPassRef = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
451            let intermediateRef = bloctoPassRef as! &BloctoPass.NFT
452            return intermediateRef
453        }
454
455        // borrowBloctoPassPrivate gets the private references to a BloctoPass NFT in the collection
456        // and returns it to the caller as a reference to the NFT
457        access(CollectionPrivateEntitlement)
458        fun borrowBloctoPassPrivate(id: UInt64): auth(BloctoPassPrivateEntitlement) &BloctoPass.NFT {
459            let bloctoPassRef = (&self.ownedNFTs[id] as auth(BloctoPassPrivateEntitlement) &{NonFungibleToken.NFT}?)!
460            return bloctoPassRef as! auth(BloctoPassPrivateEntitlement) &BloctoPass.NFT
461        }
462
463        // borrowBloctoPassPrivate gets the private references to a BloctoPass NFT in the collection
464        // and returns it to the caller as a reference to the NFT
465        access(CollectionPrivateEntitlement)
466        fun borrowWithdraw(id: UInt64): auth(FungibleToken.Withdraw) &BloctoPass.NFT {
467            let bloctoPassRef: auth(FungibleToken.Withdraw) &{NonFungibleToken.NFT} = (&self.ownedNFTs[id])!
468            return bloctoPassRef as! auth(FungibleToken.Withdraw) &BloctoPass.NFT
469        }
470        
471        access(all)
472        view fun getSupportedNFTTypes(): {Type: Bool} {
473            let supportedTypes: {Type: Bool} = {}
474            supportedTypes[Type<@BloctoPass.NFT>()] = true
475            return supportedTypes
476        }
477        
478        access(all)
479        view fun isSupportedNFTType(type: Type): Bool {
480            return type == Type<@BloctoPass.NFT>()
481        }
482        
483        access(all)
484        fun createEmptyCollection(): @{NonFungibleToken.Collection} {
485            return <-create Collection()
486        }
487    }
488
489    // public function that anyone can call to create a new empty collection
490    access(all)
491    fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
492        return <-create Collection()
493    }
494
495    access(all)
496    resource interface MinterPublic {
497        access(all)
498        fun mintBasicNFT(recipient: &{NonFungibleToken.CollectionPublic}): Void
499    }
500
501    // Resource that an admin or something similar would own to be
502    // able to mint new NFTs
503    //
504    access(all)
505    resource NFTMinter: MinterPublic {
506
507        // adds a new predefined lockup schedule
508        access(NFTMinterEntitlement)
509        fun setupPredefinedLockupSchedule(lockupSchedule: {UFix64: UFix64}) {
510            BloctoPass.predefinedLockupSchedules.append(lockupSchedule)
511            emit LockupScheduleDefined(id: BloctoPass.predefinedLockupSchedules.length, lockupSchedule: lockupSchedule)
512        }
513
514        // updates a predefined lockup schedule
515        // note that this function should be avoided 
516        access(NFTMinterEntitlement)
517        fun updatePredefinedLockupSchedule(id: Int, lockupSchedule: {UFix64: UFix64}) {
518            BloctoPass.predefinedLockupSchedules[id] = lockupSchedule
519            emit LockupScheduleUpdated(id: id, lockupSchedule: lockupSchedule)
520        }
521
522        // mintBasicNFT mints a new NFT without any special metadata or lockups
523        access(all)
524        fun mintBasicNFT(recipient: &{NonFungibleToken.CollectionPublic}) {
525            self.mintNFT(recipient: recipient, metadata:{} )
526        }
527
528        // mintNFT mints a new NFT with a new ID
529        // and deposit it in the recipients collection using their collection reference
530        access(NFTMinterEntitlement)
531        fun mintNFT(recipient: &{NonFungibleToken.CollectionPublic}, metadata: {String: String}) {
532            self.mintNFTWithCustomLockup(
533                recipient: recipient,
534                metadata: metadata,
535                vault: <- BloctoToken.createEmptyVault(vaultType: Type<@BloctoToken.Vault>()),
536                lockupSchedule: {0.0: 0.0}
537            )
538        }
539
540        access(NFTMinterEntitlement)
541        fun mintNFTWithPredefinedLockup(
542            recipient: &{NonFungibleToken.CollectionPublic},
543            metadata: {String: String},
544            vault: @{FungibleToken.Vault},
545            lockupScheduleId: Int?
546        ) {
547
548            // create a new NFT
549            var newNFT <- create NFT(
550                initID: BloctoPass.totalSupply,
551                originalOwner: recipient.owner?.address,
552                metadata: metadata,
553                vault: <- vault,
554                lockupScheduleId: lockupScheduleId,
555                lockupSchedule: nil
556            )
557
558            // deposit it in the recipient's account using their reference
559            recipient.deposit(token: <-newNFT)
560
561            BloctoPass.totalSupply = BloctoPass.totalSupply + UInt64(1)
562        }
563
564        access(NFTMinterEntitlement)
565        fun mintNFTWithCustomLockup(
566            recipient: &{NonFungibleToken.CollectionPublic},
567            metadata: {String: String},
568            vault: @{FungibleToken.Vault},
569            lockupSchedule: {UFix64: UFix64}
570        ) {
571
572            // create a new NFT
573            var newNFT <- create NFT(
574                initID: BloctoPass.totalSupply,
575                originalOwner: recipient.owner?.address,
576                metadata: metadata,
577                vault: <- vault,
578                lockupScheduleId: nil,
579                lockupSchedule: lockupSchedule
580            )
581
582            // deposit it in the recipient's account using their reference
583            recipient.deposit(token: <-newNFT)
584
585            BloctoPass.totalSupply = BloctoPass.totalSupply + UInt64(1)
586        }
587    }
588
589    access(all)
590    fun getPredefinedLockupSchedule(id: Int): {UFix64: UFix64} {
591        return self.predefinedLockupSchedules[id]
592    }
593
594    init() {
595        // Initialize the total supply
596        self.totalSupply = 0
597        self.predefinedLockupSchedules = []
598
599        self.CollectionStoragePath = /storage/bloctoPassCollection
600        self.CollectionPublicPath = /public/bloctoPassCollection
601        self.MinterStoragePath = /storage/bloctoPassMinter
602        self.MinterPublicPath = /public/bloctoPassMinter
603
604        // Create a Collection resource and save it to storage
605        let collection <- create Collection()
606        self.account.storage.save(<-collection, to: self.CollectionStoragePath)
607
608        // create a public capability for the collection
609        var capability_1 = self.account.capabilities.storage.issue<&{NonFungibleToken.CollectionPublic, BloctoPass.CollectionPublic}>(self.CollectionStoragePath)
610        self.account.capabilities.publish(capability_1, at: self.CollectionPublicPath)
611
612        // Create a Minter resource and save it to storage
613        let minter <- create NFTMinter()
614        self.account.storage.save(<-minter, to: self.MinterStoragePath)
615        emit ContractInitialized()
616    }
617
618    /// Function that returns all the Metadata Views implemented by a Non Fungible Token
619    ///
620    /// @return An array of Types defining the implemented views. This value will be used by
621    ///         developers to know which parameter to pass to the resolveView() method.
622    ///
623    access(all) view fun getContractViews(resourceType: Type?): [Type] {
624        return [
625            Type<MetadataViews.NFTCollectionData>(),
626            Type<MetadataViews.NFTCollectionDisplay>()
627        ]
628    }
629
630    /// Function that resolves a metadata view for this contract.
631    ///
632    /// @param view: The Type of the desired view.
633    /// @return A structure representing the requested view.
634    ///
635    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
636        switch viewType {
637            case Type<MetadataViews.NFTCollectionData>():
638                let collectionData = MetadataViews.NFTCollectionData(
639                    storagePath: self.CollectionStoragePath,
640                    publicPath: self.CollectionPublicPath,
641                    publicCollection: Type<&BloctoPass.Collection>(),
642                    publicLinkedType: Type<&BloctoPass.Collection>(),
643                    createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
644                        return <-BloctoPassStamp.createEmptyCollection(nftType: Type<@BloctoPass.NFT>())
645                    })
646                )
647                return collectionData
648            case Type<MetadataViews.NFTCollectionDisplay>():
649                let squareImage = MetadataViews.Media(
650                    file: MetadataViews.HTTPFile(
651                        url: "https://raw.githubusercontent.com/portto/assets-v2/master/nft/blocto-pass/logo.png"
652                    ),
653                    mediaType: "image/png"
654                )
655                let bannerImage = MetadataViews.Media(
656                    file: MetadataViews.HTTPFile(
657                        url: "https://raw.githubusercontent.com/portto/assets-v2/master/nft/blocto-pass/banner.png"
658                    ),
659                    mediaType: "image/png"
660                )
661                return MetadataViews.NFTCollectionDisplay(
662                    name: "Blcoto Pass Stamp",
663                    description: "",
664                    externalURL: MetadataViews.ExternalURL("https://blocto.io/"),
665                    squareImage: squareImage,
666                    bannerImage: bannerImage,
667                    socials: {
668                        "twitter": MetadataViews.ExternalURL("https://x.com/BloctoApp")
669                    }
670                )
671        }
672        return nil
673    } 
674}