Smart Contract
FixesTokenAirDrops
A.d2abb5dbf5e08666.FixesTokenAirDrops
1/**
2
3> Author: Fixes Lab <https://github.com/fixes-world/>
4
5# FixesTokenAirDrops
6
7This is an airdrop service contract for the FIXeS token.
8It allows users to claim airdrops of the FIXeS token.
9
10*/
11import FungibleToken from 0xf233dcee88fe0abe
12// FIXeS imports
13import Fixes from 0xd2abb5dbf5e08666
14import FixesInscriptionFactory from 0xd2abb5dbf5e08666
15import FixesFungibleTokenInterface from 0xd2abb5dbf5e08666
16import FixesTradablePool from 0xd2abb5dbf5e08666
17import FRC20Indexer from 0xd2abb5dbf5e08666
18import FRC20FTShared from 0xd2abb5dbf5e08666
19import FRC20AccountsPool from 0xd2abb5dbf5e08666
20
21/// The contract definition
22///
23access(all) contract FixesTokenAirDrops {
24
25 // ------ Events -------
26
27 // emitted when a new Drops Pool is created
28 access(all) event AirdropPoolCreated(
29 tokenType: Type,
30 tokenSymbol: String,
31 minterGrantedAmount: UFix64,
32 createdBy: Address
33 )
34
35 // emitted when the claimable amount is set
36 access(all) event AirdropPoolSetClaimable(
37 tokenType: Type,
38 tokenSymbol: String,
39 claimables: {Address: UFix64},
40 currentGrantedAmount: UFix64,
41 by: Address
42 )
43
44 // emitted when the claimable amount is claimed
45 access(all) event AirdropPoolClaimed(
46 tokenType: Type,
47 tokenSymbol: String,
48 claimer: Address,
49 claimedAmount: UFix64,
50 )
51
52 /// -------- Resources and Interfaces --------
53
54 /// The Airdrop Pool public resource interface
55 ///
56 access(all) resource interface AirdropPoolPoolPublic {
57 // ----- Basics -----
58
59 /// Get the subject address
60 access(all)
61 view fun getPoolAddress(): Address {
62 return self.owner?.address ?? panic("The owner is missing")
63 }
64
65 // Borrow the tradable pool
66 access(all)
67 view fun borrowRelavantTradablePool(): &FixesTradablePool.TradableLiquidityPool? {
68 return FixesTradablePool.borrowTradablePool(self.getPoolAddress())
69 }
70
71 /// Check if the pool is claimable
72 access(all)
73 view fun isClaimable(): Bool
74
75 // ----- Token in the drops pool -----
76
77 /// Get the total claimable amount
78 access(all)
79 view fun getTotalClaimableAmount(): UFix64
80
81 /// Get the claimable amount
82 access(all)
83 view fun getClaimableTokenAmount(_ userAddr: Address): UFix64
84
85 // --- Writable ---
86
87 /// Set the claimable amount
88 access(all)
89 fun setClaimableDict(
90 _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
91 claimables: {Address: UFix64}
92 ) {
93 pre {
94 ins.isExtractable(): "The inscription is not extractable"
95 }
96 post {
97 ins.isExtracted(): "The inscription is not extracted"
98 }
99 }
100
101 /// Claim drops token
102 access(all)
103 fun claimDrops(
104 _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
105 recipient: &{FungibleToken.Receiver},
106 ) {
107 pre {
108 ins.isExtractable(): "The inscription is not extractable"
109 self.isClaimable(): "You can not claim the token when the pool is not claimable"
110 }
111 post {
112 ins.isExtracted(): "The inscription is not extracted"
113 }
114 }
115 }
116
117 /// The Airdrop Pool resource
118 ///
119 access(all) resource AirdropPool: AirdropPoolPoolPublic, FixesFungibleTokenInterface.IMinterHolder {
120 // The minter of the token
121 access(self)
122 let minter: @{FixesFungibleTokenInterface.IMinter}
123 // Address => Record
124 access(self)
125 let claimableRecords: {Address: UFix64}
126 // Granted amount
127 access(self)
128 var grantedClaimableAmount: UFix64
129
130 init(
131 _ minter: @{FixesFungibleTokenInterface.IMinter},
132 ) {
133 pre {
134 minter.getTotalAllowedMintableAmount() > 0.0: "The mint amount must be greater than 0"
135 }
136 self.minter <- minter
137 self.claimableRecords = {}
138 self.grantedClaimableAmount = 0.0
139 }
140
141 // ------ Implment AirdropPoolPoolPublic ------
142
143 /// Check if the pool is claimable
144 access(all)
145 view fun isClaimable(): Bool {
146 // check if tradable pool exists
147 // the drops pool is activated only when the tradable pool is initialized but not active
148 if let tradablePool = self.borrowRelavantTradablePool() {
149 return tradablePool.isInitialized() && !tradablePool.isLocalActive()
150 }
151 return true
152 }
153
154 // ----- Token in the drops pool -----
155
156 /// Get the total claimable amount
157 access(all)
158 view fun getTotalClaimableAmount(): UFix64 {
159 return self.grantedClaimableAmount
160 }
161
162 /// Get the claimable amount
163 access(all)
164 view fun getClaimableTokenAmount(_ userAddr: Address): UFix64 {
165 return self.claimableRecords[userAddr] ?? 0.0
166 }
167
168 // --- Writable ---
169
170 /// Set the claimable amount
171 access(all)
172 fun setClaimableDict(
173 _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
174 claimables: {Address: UFix64}
175 ) {
176 pre {
177 self.getTotalClaimableAmount() < self.getTotalAllowedMintableAmount(): "The total claimable amount exceeds the mintable amount"
178 }
179 let callerAddr = ins.owner?.address ?? panic("The owner is missing")
180 assert(
181 self.isAuthorizedUser(callerAddr),
182 message: "The caller is not an authorized user"
183 )
184
185 // extract the inscription
186 let _meta = FixesTradablePool.verifyAndExecuteInscription(
187 ins,
188 symbol: self.minter.getSymbol(),
189 usage: "set-claimables"
190 )
191
192 // Total claimable amount
193 let totalClaimableAmount = self.minter.getCurrentMintableAmount()
194 let oldGrantedClaimableAmount = self.grantedClaimableAmount
195 var newGrantedClaimableAmount = oldGrantedClaimableAmount
196
197 // set the claimable amount
198 for addr in claimables.keys {
199 if let amount = claimables[addr] {
200 self.claimableRecords[addr] = amount + (self.claimableRecords[addr] ?? 0.0)
201 newGrantedClaimableAmount = newGrantedClaimableAmount + amount
202 }
203 }
204 assert(
205 newGrantedClaimableAmount <= totalClaimableAmount,
206 message: "The total claimable amount exceeds the mintable amount"
207 )
208 self.grantedClaimableAmount = newGrantedClaimableAmount
209 log("The granted claimable amount is updated from ".concat(oldGrantedClaimableAmount.toString())
210 .concat(" to ").concat(newGrantedClaimableAmount.toString()))
211
212 // emit the event
213 emit AirdropPoolSetClaimable(
214 tokenType: self.minter.getTokenType(),
215 tokenSymbol: self.minter.getSymbol(),
216 claimables: claimables,
217 currentGrantedAmount: newGrantedClaimableAmount,
218 by: callerAddr
219 )
220 }
221
222 /// Claim drops token
223 access(all)
224 fun claimDrops(
225 _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
226 recipient: &{FungibleToken.Receiver},
227 ) {
228 let callerAddr = ins.owner?.address ?? panic("The owner is missing")
229 assert(
230 callerAddr == recipient.owner?.address,
231 message: "The caller is not the recipient"
232 )
233
234 let claimableAmount = self.getClaimableTokenAmount(callerAddr)
235
236 assert(
237 claimableAmount > 0.0,
238 message: "The caller has no claimable amount"
239 )
240
241 let supportTypes = recipient.getSupportedVaultTypes()
242 assert(
243 supportTypes[self.minter.getTokenType()] == true,
244 message: "The recipient does not support the token"
245 )
246
247 // initialize the vault by inscription, op=exec
248 let vaultData = self.minter.getVaultData()
249 let initializedVault <- self.minter.initializeVaultByInscription(
250 vault: <- vaultData.createEmptyVault(),
251 ins: ins
252 )
253 // mint the token
254 initializedVault.deposit(from: <- self.minter.mintTokens(amount: claimableAmount))
255 // update the claimable amount
256 self.claimableRecords[callerAddr] = 0.0
257
258 // transfer the token
259 recipient.deposit(from: <- initializedVault)
260
261 // emit the event
262 emit AirdropPoolClaimed(
263 tokenType: self.minter.getTokenType(),
264 tokenSymbol: self.minter.getSymbol(),
265 claimer: callerAddr,
266 claimedAmount: claimableAmount
267 )
268 }
269
270 // ------ Implment FixesFungibleTokenInterface.IMinterHolder ------
271
272 /// Get the circulating supply of the token
273 access(all)
274 view fun getCirculatingSupply(): UFix64 {
275 if !self.isClaimable() {
276 if let tradablePool = self.borrowRelavantTradablePool() {
277 return tradablePool.getTradablePoolCirculatingSupply()
278 } else {
279 return self.minter.getTotalSupply()
280 }
281 } else {
282 return self.minter.getTotalSupply()
283 }
284 }
285
286 /// Borrow the minter
287 access(contract)
288 view fun borrowMinter(): auth(FixesFungibleTokenInterface.Manage) &{FixesFungibleTokenInterface.IMinter} {
289 return &self.minter
290 }
291
292 // ----- Internal Methods -----
293
294 /// Check if the caller is an authorized user
295 ///
296 access(self)
297 view fun isAuthorizedUser(_ callerAddr: Address): Bool {
298 // singleton resources
299 let acctsPool = FRC20AccountsPool.borrowAccountsPool()
300 // The caller should be authorized user for the token
301 let key = self.minter.getAccountsPoolKey() ?? panic("The accounts pool key is missing")
302 // borrow the contract
303 let contractRef = acctsPool.borrowFTContract(key) ?? panic("The contract is missing")
304 let globalPublicRef = contractRef.borrowGlobalPublic()
305 return globalPublicRef.isAuthorizedUser(callerAddr)
306 }
307 }
308
309
310 /// ------ Public Methods ------
311
312 /// Create a new Airdrop Pool
313 ///
314 access(account)
315 fun createDropsPool(
316 _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
317 _ minter: @{FixesFungibleTokenInterface.IMinter},
318 ): @AirdropPool {
319 post {
320 ins.isValueEmpty(): "The inscription is not extracted"
321 }
322
323 // verify the inscription and get the meta data
324 let meta = FixesTradablePool.verifyAndExecuteInscription(
325 ins,
326 symbol: minter.getSymbol(),
327 usage: "*"
328 )
329
330 let tokenType = minter.getTokenType()
331 let tokenSymbol = minter.getSymbol()
332 let grantedAmount = minter.getCurrentMintableAmount()
333
334 let pool <- create AirdropPool(<- minter)
335
336 // emit the event
337 emit AirdropPoolCreated(
338 tokenType: tokenType,
339 tokenSymbol: tokenSymbol,
340 minterGrantedAmount: grantedAmount,
341 createdBy: ins.owner?.address ?? panic("The inscription owner is missing")
342 )
343
344 return <- pool
345 }
346
347 /// Borrow the Drops Pool
348 ///
349 access(all)
350 view fun borrowAirdropPool(_ addr: Address): &AirdropPool? {
351 return getAccount(addr)
352 .capabilities.get<&AirdropPool>(self.getAirdropPoolPublicPath())
353 .borrow()
354 }
355
356 /// Get the prefix for the storage paths
357 ///
358 access(all)
359 view fun getPathPrefix(): String {
360 return "FixesAirDrops_".concat(self.account.address.toString()).concat("_")
361 }
362
363 /// Get the storage path for the Locking Center
364 ///
365 access(all)
366 view fun getAirdropPoolStoragePath(): StoragePath {
367 let prefix = self.getPathPrefix()
368 return StoragePath(identifier: prefix.concat("Pool"))!
369 }
370
371 /// Get the public path for the Liquidity Pool
372 ///
373 access(all)
374 view fun getAirdropPoolPublicPath(): PublicPath {
375 let prefix = self.getPathPrefix()
376 return PublicPath(identifier: prefix.concat("Pool"))!
377 }
378}
379