Smart Contract

FixesTokenAirDrops

A.d2abb5dbf5e08666.FixesTokenAirDrops

Valid From

86,129,162

Deployed

3d ago
Feb 24, 2026, 11:54:28 PM UTC

Dependents

0 imports
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