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