Smart Contract

BloctoTokenSale

A.0f9df91c9121c460.BloctoTokenSale

Deployed

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

Dependents

0 imports
1/*
2
3    BloctoTokenSale
4
5    The BloctoToken Sale contract is used for 
6    BLT token community sale. Qualified purchasers
7    can purchase with tUSDT (Teleported Tether) to get
8    BLTs at the same price and lock-up terms as private sale
9
10 */
11 
12import FungibleToken from 0xf233dcee88fe0abe
13import NonFungibleToken from 0x1d7e57aa55817448
14import BloctoToken from 0x0f9df91c9121c460
15import BloctoPass from 0x0f9df91c9121c460
16import TeleportedTetherToken from 0xcfdd90d4a00f7b5b
17
18pub contract BloctoTokenSale {
19
20    /****** Sale Events ******/
21
22    pub event NewPrice(price: UFix64)
23    pub event NewLockupSchedule(lockupSchedule: {UFix64: UFix64})
24    pub event NewPersonalCap(personalCap: UFix64)
25
26    pub event Purchased(address: Address, amount: UFix64, ticketId: UInt64)
27    pub event Distributed(address: Address, tusdtAmount: UFix64, bltAmount: UFix64)
28    pub event Refunded(address: Address, amount: UFix64)
29
30    /****** Sale Enums ******/
31
32    pub enum PurchaseState: UInt8 {
33        pub case initial
34        pub case distributed
35        pub case refunded
36    }
37
38    /****** Sale Resources ******/
39
40    // BLT holder vault
41    access(contract) let bltVault: @BloctoToken.Vault
42
43    // tUSDT holder vault
44    access(contract) let tusdtVault: @TeleportedTetherToken.Vault
45
46    /// Paths for storing sale resources
47    pub let SaleAdminStoragePath: StoragePath
48    
49    /****** Sale Variables ******/
50
51    access(contract) var isSaleActive: Bool
52
53    // BLT token price (tUSDT per BLT)
54    access(contract) var price: UFix64
55
56    // BLT lockup schedule, used for lockup terms
57    access(contract) var lockupScheduleId: Int
58
59    // BLT communitu sale purchase cap (in tUSDT)
60    access(contract) var personalCap: UFix64
61
62    // All purchase records
63    access(contract) var purchases: {Address: PurchaseInfo}
64
65    pub struct PurchaseInfo {
66        // Purchaser address
67        pub let address: Address
68
69        // Purchase amount in tUSDT
70        pub let amount: UFix64
71
72        // Random ticked ID
73        pub let ticketId: UInt64
74
75        // State of the purchase
76        pub(set) var state: PurchaseState
77
78        init(
79            address: Address,
80            amount: UFix64,
81        ) {
82            self.address = address
83            self.amount = amount
84            self.ticketId = unsafeRandom() % 1_000_000_000
85            self.state = PurchaseState.initial
86        }
87    }
88
89    // BLT purchase method
90    // User pays tUSDT and get a BloctoPass NFT with lockup terms
91    // Note that "address" can potentially be faked, but there's no incentive doing so
92    pub fun purchase(from: @TeleportedTetherToken.Vault, address: Address) {
93        pre {
94            self.isSaleActive: "Token sale is not active"
95            self.purchases[address] == nil: "Already purchased by the same account"
96            from.balance <= self.personalCap: "Purchase amount exceeds personal cap"
97        }
98
99        let collectionRef = getAccount(address).getCapability(BloctoPass.CollectionPublicPath)
100            .borrow<&{NonFungibleToken.CollectionPublic}>()
101            ?? panic("Could not borrow blocto pass collection public reference")
102
103        // Make sure user does not already have a BloctoPass
104        assert (
105            collectionRef.getIDs().length == 0,
106            message: "User already has a BloctoPass"
107        )
108
109        let amount = from.balance
110        self.tusdtVault.deposit(from: <- from)
111
112        let purchaseInfo = PurchaseInfo(address: address, amount: amount)
113        self.purchases[address] = purchaseInfo
114
115        emit Purchased(address: address, amount: amount, ticketId: purchaseInfo.ticketId)
116    }
117
118    pub fun getIsSaleActive(): Bool {
119        return self.isSaleActive
120    }
121
122    // Get all purchaser addresses
123    pub fun getPurchasers(): [Address] {
124        return self.purchases.keys
125    }
126
127    // Get all purchase records
128    pub fun getPurchases(): {Address: PurchaseInfo} {
129        return self.purchases
130    }
131
132    // Get purchase record from an address
133    pub fun getPurchase(address: Address): PurchaseInfo? {
134        return self.purchases[address]
135    }
136
137    pub fun getBltVaultBalance(): UFix64 {
138        return self.bltVault.balance
139    }
140
141    pub fun getTusdtVaultBalance(): UFix64 {
142        return self.tusdtVault.balance
143    }
144
145    pub fun getPrice(): UFix64 {
146        return self.price
147    }
148
149    pub fun getLockupSchedule(): {UFix64: UFix64} {
150        return BloctoPass.getPredefinedLockupSchedule(id: self.lockupScheduleId)
151    }
152
153    pub fun getPersonalCap(): UFix64 {
154        return self.personalCap
155    }
156
157    pub resource Admin {
158        pub fun unfreeze() {
159            BloctoTokenSale.isSaleActive = true
160        }
161
162        pub fun freeze() {
163            BloctoTokenSale.isSaleActive = false
164        }
165
166        pub fun distribute(address: Address) {
167            pre {
168                BloctoTokenSale.purchases[address] != nil: "Cannot find purchase record for the address"
169                BloctoTokenSale.purchases[address]?.state == PurchaseState.initial: "Already distributed or refunded"
170            }
171
172            let collectionRef = getAccount(address).getCapability(BloctoPass.CollectionPublicPath)
173                .borrow<&{NonFungibleToken.CollectionPublic}>()
174                ?? panic("Could not borrow blocto pass collection public reference")
175
176            // Make sure user does not already have a BloctoPass
177            assert (
178                collectionRef.getIDs().length == 0,
179                message: "User already has a BloctoPass"
180            )
181
182            let purchaseInfo = BloctoTokenSale.purchases[address]
183                ?? panic("Count not get purchase info for the address")
184        
185            let minterRef = BloctoTokenSale.account.borrow<&BloctoPass.NFTMinter>(from: BloctoPass.MinterStoragePath)
186                ?? panic("Could not borrow reference to the BloctoPass minter!")
187
188            let bltAmount = purchaseInfo.amount / BloctoTokenSale.price
189            let bltVault <- BloctoTokenSale.bltVault.withdraw(amount: bltAmount)
190
191            let metadata = {
192                "origin": "Community Sale"
193            }
194
195            // Lockup schedule for community sale:
196            // let lockupSchedule = {
197            //     0.0                      : 1.0,
198            //     saleDate                 : 1.0,
199            //     saleDate + 6.0 * months  : 17.0 / 18.0,
200            //     saleDate + 7.0 * months  : 16.0 / 18.0,
201            //     saleDate + 8.0 * months  : 15.0 / 18.0,
202            //     saleDate + 9.0 * months  : 14.0 / 18.0,
203            //     saleDate + 10.0 * months : 13.0 / 18.0,
204            //     saleDate + 11.0 * months : 12.0 / 18.0,
205            //     saleDate + 12.0 * months : 11.0 / 18.0,
206            //     saleDate + 13.0 * months : 10.0 / 18.0,
207            //     saleDate + 14.0 * months : 9.0 / 18.0,
208            //     saleDate + 15.0 * months : 8.0 / 18.0,
209            //     saleDate + 16.0 * months : 7.0 / 18.0,
210            //     saleDate + 17.0 * months : 6.0 / 18.0,
211            //     saleDate + 18.0 * months : 5.0 / 18.0,
212            //     saleDate + 19.0 * months : 4.0 / 18.0,
213            //     saleDate + 20.0 * months : 3.0 / 18.0,
214            //     saleDate + 21.0 * months : 2.0 / 18.0,
215            //     saleDate + 22.0 * months : 1.0 / 18.0,
216            //     saleDate + 23.0 * months : 0.0
217            // }
218
219            // Set the state of the purchase to DISTRIBUTED
220            purchaseInfo.state = PurchaseState.distributed
221            BloctoTokenSale.purchases[address] = purchaseInfo
222
223            minterRef.mintNFTWithPredefinedLockup(
224                recipient: collectionRef,
225                metadata: metadata,
226                vault: <- bltVault,
227                lockupScheduleId: BloctoTokenSale.lockupScheduleId
228            )
229
230            emit Distributed(address: address, tusdtAmount: purchaseInfo.amount, bltAmount: bltAmount)
231        }
232
233        pub fun refund(address: Address) {
234            pre {
235                BloctoTokenSale.purchases[address] != nil: "Cannot find purchase record for the address"
236                BloctoTokenSale.purchases[address]?.state == PurchaseState.initial: "Already distributed or refunded"
237            }
238
239            let receiverRef = getAccount(address).getCapability(TeleportedTetherToken.TokenPublicReceiverPath)
240                .borrow<&{FungibleToken.Receiver}>()
241                ?? panic("Could not borrow tUSDT vault receiver public reference")
242
243            let purchaseInfo = BloctoTokenSale.purchases[address]
244                ?? panic("Count not get purchase info for the address")
245
246            let tusdtVault <- BloctoTokenSale.tusdtVault.withdraw(amount: purchaseInfo.amount)
247
248            // Set the state of the purchase to REFUNDED
249            purchaseInfo.state = PurchaseState.refunded
250            BloctoTokenSale.purchases[address] = purchaseInfo
251
252            receiverRef.deposit(from: <- tusdtVault)
253
254            emit Refunded(address: address, amount: purchaseInfo.amount)
255        }
256
257        pub fun updatePrice(price: UFix64) {
258            pre {
259                price > 0.0: "Sale price cannot be 0"
260            }
261
262            BloctoTokenSale.price = price
263            emit NewPrice(price: price)
264        }
265
266        pub fun updateLockupScheduleId(lockupScheduleId: Int) {
267            BloctoTokenSale.lockupScheduleId = lockupScheduleId
268            emit NewLockupSchedule(lockupSchedule: BloctoPass.getPredefinedLockupSchedule(id: lockupScheduleId))
269        }
270
271        pub fun updatePersonalCap(personalCap: UFix64) {
272            BloctoTokenSale.personalCap = personalCap
273            emit NewPersonalCap(personalCap: personalCap)
274        }
275
276        pub fun withdrawBlt(amount: UFix64): @FungibleToken.Vault {
277            return <- BloctoTokenSale.bltVault.withdraw(amount: amount)
278        }
279
280        pub fun withdrawTusdt(amount: UFix64): @FungibleToken.Vault {
281            return <- BloctoTokenSale.tusdtVault.withdraw(amount: amount)
282        }
283
284        pub fun depositBlt(from: @FungibleToken.Vault) {
285            BloctoTokenSale.bltVault.deposit(from: <- from)
286        }
287
288        pub fun depositTusdt(from: @FungibleToken.Vault) {
289            BloctoTokenSale.tusdtVault.deposit(from: <- from)
290        }
291    }
292
293    init() {
294        // Needs Admin to start manually
295        self.isSaleActive = false
296
297        // 1 BLT = 0.1 tUSDT
298        self.price = 0.1
299
300        // Refer to BloctoPass contract
301        self.lockupScheduleId = 0
302
303        // Each user can purchase at most 1000 tUSDT worth of BLT
304        self.personalCap = 1000.0
305
306        self.purchases = {}
307        self.SaleAdminStoragePath = /storage/bloctoTokenSaleAdmin
308
309        self.bltVault <- BloctoToken.createEmptyVault() as! @BloctoToken.Vault
310        self.tusdtVault <- TeleportedTetherToken.createEmptyVault() as! @TeleportedTetherToken.Vault
311
312        let admin <- create Admin()
313        self.account.save(<- admin, to: self.SaleAdminStoragePath)
314    }
315}
316