Smart Contract
BloctoPass
A.0f9df91c9121c460.BloctoPass
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}