Smart Contract

BlackHole

A.4396883a58c3a2d1.BlackHole

Valid From

86,036,548

Deployed

3d ago
Feb 24, 2026, 11:57:46 PM UTC

Dependents

0 imports
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# Black Hole is the utility contract for burning fungible tokens on the Flow blockchain.
5
6## Features:
7
8- You can register a BlackHole Resource from the BlackHole contract.
9- Users can burn fungible tokens by sending them to the random BlackHole Resource.
10- Users can get the balance of vanished fungible tokens by the type of the Fungible Token in the BlackHole Resource.
11
12*/
13import FungibleToken from 0xf233dcee88fe0abe
14import NonFungibleToken from 0x1d7e57aa55817448
15import ViewResolver from 0x1d7e57aa55817448
16import StringUtils from 0xa340dc0a4ec828ab
17// IncrementFi Swap
18import SwapConfig from 0xb78ef7afa52ff906
19import SwapInterfaces from 0xb78ef7afa52ff906
20
21/// BlackHole contract
22///
23access(all) contract BlackHole {
24
25    /* --- Entitlement --- */
26
27    // NOTHING
28
29    /* --- Events --- */
30
31    /// Event emitted when a new BlackHole Resource is registered
32    access(all) event NewBlackHoleRegistered(
33        blackHoleAddr: Address,
34        blackHoleId: UInt64,
35    )
36
37    /// Event emitted when a new Fungible Token is registered
38    access(all) event FungibleTokenVanished(
39        blackHoleAddr: Address,
40        blackHoleId: UInt64,
41        vaultIdentifier: Type,
42        amount: UFix64,
43    )
44
45    /// Event emitted when a new NonFungible Token is registered
46    access(all) event NonFungibleTokenVanished(
47        blackHoleAddr: Address,
48        blackHoleId: UInt64,
49        nftIdentifier: Type,
50        nftID: UInt64,
51    )
52
53    /* --- Variable, Enums and Structs --- */
54
55    /// BlackHole Resource
56    access(all) let storagePath: StoragePath
57    /// BlackHoles Registry
58    access(contract) let blackHoles: {Address: Bool}
59
60    /* --- Interfaces & Resources --- */
61
62    /// The public interface for the BlackHole Resource
63    ///
64    access(all) resource interface BlackHolePublic {
65        /// Check if the BlackHole Resource is valid
66        /// Valid means that the owner's account should have all keys revoked
67        ///
68        access(all)
69        view fun isValid(): Bool {
70            /// The Keys in the owner's account should be all revoked
71            if let ownerAddr = self.owner?.address {
72                let ownerAcct = getAccount(ownerAddr)
73                // Check if all keys are revoked
74                var isAllKeyRevoked = true
75                let totalKeyAmount = Int(ownerAcct.keys.count)
76                var i = 0
77                while i < totalKeyAmount {
78                    if let key = ownerAcct.keys.get(keyIndex: i) {
79                        isAllKeyRevoked = isAllKeyRevoked && key.isRevoked
80                    }
81                    i = i + 1
82                }
83                // TODO: Check no owned account (Hybrid custodial account)
84
85                return isAllKeyRevoked
86            }
87            return false
88        }
89    }
90
91    /// The resource of BlackHole Fungible Token Receiver
92    ///
93    access(all) resource Receiver: FungibleToken.Receiver, BlackHolePublic {
94        /// The dictionary of Fungible Token Pools
95        access(self) let pools: @{Type: {FungibleToken.Vault}}
96
97        init() {
98            self.pools <- {}
99        }
100
101        /** ---- FungibleToken Receiver Interface ---- */
102
103        /// Takes a Vault and deposits it into the implementing resource type
104        ///
105        /// @param from: The Vault resource containing the funds that will be deposited
106        ///
107        access(all)
108        fun deposit(from: @{FungibleToken.Vault}) {
109            pre {
110                self.isValid(): "The BlackHole Resource should be valid"
111                from.balance > 0.0: "The balance should be greater than zero"
112            }
113            let blackHoleAddr = self.owner?.address ?? panic("Invalid BlackHole Address")
114
115            // get basic information
116            let fromType = from.getType()
117            let vanishedAmount = from.balance
118
119            // should be A.{address}.{contractName}.Vault
120            let fromIdentifierArr = StringUtils.split(fromType.identifier, ".")
121            // check if the from vault is an IncrementFi LP
122            if fromIdentifierArr[2] == "SwapPair" {
123                let pairAddr = Address.fromString("0x".concat(fromIdentifierArr[1]))!
124                // @deprecated in Cadence 1.0
125                if let pairPubRef = getAccount(pairAddr)
126                    .capabilities
127                    .borrow<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath) {
128                    if pairPubRef.getLpTokenVaultType() == fromType {
129                        // Now we can confirm that the from vault is an IncrementFi LP
130                        // check if there is a LP Collection in the BlackHole Account
131                        if let lpTokenCollectionRef = getAccount(blackHoleAddr)
132                            .capabilities
133                            .borrow<&{SwapInterfaces.LpTokenCollectionPublic}>(SwapConfig.LpTokenCollectionPublicPath) {
134                            // Deposit the LP Token into the LP Collection
135                            lpTokenCollectionRef.deposit(pairAddr: pairAddr, lpTokenVault: <- from)
136
137                            emit BlackHole.FungibleTokenVanished(
138                                blackHoleAddr: blackHoleAddr,
139                                blackHoleId: self.uuid,
140                                vaultIdentifier: fromType,
141                                amount: vanishedAmount
142                            )
143                            return
144                        }
145                    }
146                }
147            }
148            // Deposit the Fungible Token into the BlackHole Vault
149            let receiverRef = self._borrowOrCreateBlackHoleVault(fromType)
150            receiverRef.deposit(from: <- from)
151
152            emit BlackHole.FungibleTokenVanished(
153                blackHoleAddr: blackHoleAddr,
154                blackHoleId: self.uuid,
155                vaultIdentifier: fromType,
156                amount: vanishedAmount
157            )
158        }
159
160        /// getSupportedVaultTypes returns a dictionary of Vault types
161        /// and whether the type is currently supported by this Receiver
162        access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
163            // All Vault types are supported by default, so return an empty dictionary
164            return {}
165        }
166
167        /// Returns whether or not the given type is accepted by the Receiver
168        /// A vault that can accept any type should just return true by default
169        access(all) view fun isSupportedVaultType(type: Type): Bool {
170            return true
171        }
172
173        /** ---- BlackHolePublic Interface ---- */
174
175        /// Get the balance by the type of the Fungible Token
176        ///
177        access(all)
178        view fun getVanishedBalance(_ type: Type): UFix64 {
179            return self.pools[type]?.balance ?? 0.0
180        }
181
182        /** ---- Internal Methods ---- */
183
184        /// Borrow the FungibleToken Vault
185        ///
186        access(self)
187        fun _borrowOrCreateBlackHoleVault(_ type: Type): &{FungibleToken.Vault} {
188            pre {
189                type.isSubtype(of: Type<@{FungibleToken.Vault}>()): "The type should be a subtype of FungibleToken.Vault"
190            }
191            if let ref = &self.pools[type] as &{FungibleToken.Vault}? {
192                return ref
193            } else {
194                let ftArr = StringUtils.split(type.identifier, ".")
195                let ftAddress = Address.fromString("0x".concat(ftArr[1])) ?? panic("Invalid Fungible Token Address")
196                let ftContractName = ftArr[2]
197                let ftContract = getAccount(ftAddress)
198                    .contracts.borrow<&{FungibleToken}>(name: ftContractName)
199                    ?? panic("Could not borrow the FungibleToken contract reference")
200                // @deprecated in Cadence 1.0
201                self.pools[type] <-! ftContract.createEmptyVault(vaultType: type)
202                return &self.pools[type] as &{FungibleToken.Vault}? ?? panic("Invalid Fungible Token Vault")
203            }
204        }
205    }
206
207    /// The resource of BlackHole NonFungible Token Collection
208    ///
209    access(all) resource Collection: NonFungibleToken.Collection, BlackHolePublic {
210        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
211        /// The dictionary of NonFungible Token Pools
212        /// NFT Type -> [NFT ID]
213        access(self) let nftOriginIds: {Type: [UInt64]}
214        access(self) let nftIdPrefix: {Type: UInt64}
215        access(self) let supportedNftTypes: {Type: Bool}
216
217        init() {
218            self.ownedNFTs <- {}
219            self.nftOriginIds = {}
220            self.nftIdPrefix = {}
221            self.supportedNftTypes = {}
222        }
223
224        /// --- NonFungibleToken Collection Interface ---
225
226        access(NonFungibleToken.Withdraw)
227        fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
228            panic("This function is invalid for the BlackHole Collection")
229        }
230
231        /// getSupportedNFTTypes returns a dictionary of NFT types
232        /// It returns all types of the vanished NFTs
233        access(all)
234        view fun getSupportedNFTTypes(): {Type: Bool} {
235            return self.supportedNftTypes
236        }
237
238        /// All NFT types are supported by default, so return true by default
239        ///
240        access(all)
241        view fun isSupportedNFTType(type: Type): Bool {
242            return true
243        }
244
245        /// deposit takes a NFT as an argument and stores it in the collection
246        /// @param token: The NFT to deposit into the collection
247        access(all)
248        fun deposit(token: @{NonFungibleToken.NFT}) {
249            pre {
250                self.isValid(): "The BlackHole Resource should be valid"
251            }
252
253            let nftType = token.getType()
254            var nftIdPrefix: UInt64 = 0
255            if self.nftIdPrefix[nftType] != nil {
256                nftIdPrefix = self.nftIdPrefix[nftType]!
257            } else {
258                nftIdPrefix = (UInt64(self.nftIdPrefix.keys.length) + 1) << 32
259                self.nftIdPrefix[nftType] = nftIdPrefix
260            }
261
262            if self.nftOriginIds[nftType] == nil {
263                self.nftOriginIds[nftType] = []
264            }
265            let nftIds = &self.nftOriginIds[nftType] as auth(Mutate) &[UInt64]? ?? panic("Invalid NFT Origin IDs")
266            let nftIdToVanish: UInt64 = token.id
267            // Add the NFT ID to the NFT Origin IDs
268            nftIds.append(nftIdToVanish)
269
270            // Store the NFT in the collection
271            let newNFTid = nftIdPrefix + nftIdToVanish
272            let toDestory <- self.ownedNFTs[newNFTid] <- token
273            destroy toDestory
274
275            // Set the NFTType as supported
276            if self.supportedNftTypes[nftType] == nil {
277                self.supportedNftTypes[nftType] = true
278            }
279
280            emit BlackHole.NonFungibleTokenVanished(
281                blackHoleAddr: self.owner?.address ?? panic("Invalid BlackHole Address"),
282                blackHoleId: self.uuid,
283                nftIdentifier: nftType,
284                nftID: nftIdToVanish
285            )
286        }
287
288        access(all)
289        view fun getLength(): Int {
290            return self.ownedNFTs.length
291        }
292
293        access(all)
294        view fun getIDs(): [UInt64] {
295            return self.ownedNFTs.keys
296        }
297
298        access(all)
299        view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
300            return &self.ownedNFTs[id]
301        }
302
303        /// Borrow the view resolver for the specified NFT ID
304        access(all)
305        view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
306            if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
307                return nft as &{ViewResolver.Resolver}
308            }
309            return nil
310        }
311
312        access(all)
313        fun createEmptyCollection(): @{NonFungibleToken.Collection} {
314            return <- create Collection()
315        }
316
317        /// --- BlackHolePublic Interface ---
318
319        /// Get the balance by the type of the Fungible Token
320        ///
321        access(all)
322        view fun getVanishedAmount(_ type: Type): Int {
323            return self.nftOriginIds[type]?.length ?? 0
324        }
325    }
326
327    /** --- Methods --- */
328
329    /// Get the receiver path for the BlackHole Resource
330    ///
331    /// @return The PublicPath for the generic BlackHole receiver
332    ///
333    access(all)
334    view fun getBlackHoleReceiverPublicPath(): PublicPath {
335        return /public/BlackHoleFTReceiver
336    }
337
338    /// Get the storage path for the BlackHole Resource
339    ///
340    /// @return The StoragePath for the generic BlackHole receiver
341    ///
342    access(all)
343    view fun getBlackHoleReceiverStoragePath(): StoragePath {
344        return self.storagePath
345    }
346
347    /// Create a new BlackHole Resource
348    ///
349    access(all)
350    fun createNewBlackHole(): @Receiver {
351        return <- create Receiver()
352    }
353
354    /// Register an address as a new BlackHole
355    ///
356    access(all)
357    fun registerAsBlackHole(_ addr: Address) {
358        if self.blackHoles[addr] == nil {
359            let ref = self.borrowBlackHoleReceiver(addr)
360                ?? panic("Could not borrow the BlackHole Resource")
361            assert(
362                ref.isValid(),
363                message: "The BlackHole Resource should be valid"
364            )
365            self.blackHoles[addr] = true
366
367            // emit the event
368            emit NewBlackHoleRegistered(
369                blackHoleAddr: addr,
370                blackHoleId: ref.uuid
371            )
372        }
373    }
374
375    /// Borrow a BlackHole Resource by the address
376    ///
377    access(all)
378    view fun borrowBlackHoleReceiver(_ addr: Address): &{FungibleToken.Receiver, BlackHolePublic}? {
379        return getAccount(addr)
380            .capabilities.borrow<&Receiver>(self.getBlackHoleReceiverPublicPath())
381    }
382
383    /// Check if is the address a valid BlackHole address
384    ///
385    access(all)
386    view fun isValidBlackHole(_ addr: Address): Bool {
387        return self.borrowBlackHoleReceiver(addr)?.isValid() == true
388    }
389
390    /// Register a BlackHole Resource
391    ///
392    access(all)
393    fun borrowRandomBlackHoleReceiver(): &{FungibleToken.Receiver, BlackHolePublic} {
394        let max = UInt64(self.blackHoles.keys.length)
395        assert(max > 0, message: "There is no BlackHole Resource")
396        let rand = revertibleRandom<UInt64>()
397        let blackHoleAddr = self.blackHoles.keys[rand % max]
398        return self.borrowBlackHoleReceiver(blackHoleAddr) ?? panic("Could not borrow the BlackHole Resource")
399    }
400
401    /// Get the registered BlackHoles addresses
402    ///
403    access(all)
404    view fun getRegisteredBlackHoles(): [Address] {
405        return self.blackHoles.keys
406    }
407
408    /// Check if there is any BlackHole Resource available
409    ///
410    access(all)
411    view fun isAnyBlackHoleAvailable(): Bool {
412        return self.blackHoles.keys.length > 0
413    }
414
415    /// Burn the Fungible Token by sending it to the BlackHole Resource
416    ///
417    access(all)
418    fun vanish(_ vault: @{FungibleToken.Vault}) {
419        let blackHole = self.borrowRandomBlackHoleReceiver()
420        blackHole.deposit(from: <- vault)
421    }
422
423    /// ----- For BlackHole NFT Collection -----
424
425    /// Get the public path for the BlackHole Collection
426    ///
427    /// @return The PublicPath for the BlackHole Collection
428    ///
429    access(all)
430    view fun getBlackHoleCollectionPublicPath(): PublicPath {
431        return /public/BlackHoleNFTCollection
432    }
433
434    /// Get the storage path for the BlackHole Collection
435    ///
436    /// @return The StoragePath for the BlackHole Collection
437    ///
438    access(all)
439    view fun getBlackHoleCollectionStoragePath(): StoragePath {
440        return /storage/BlackHoleNFTCollection
441    }
442
443    /// Create a new BlackHole Resource
444    ///
445    access(all)
446    fun createNewBlackHoleCollection(): @Collection {
447        return <- create Collection()
448    }
449
450    /// Borrow a BlackHole Resource by the address
451    ///
452    access(all)
453    view fun borrowBlackHoleCollection(_ addr: Address): &Collection? {
454        return getAccount(addr)
455            .capabilities
456            .borrow<&Collection>(self.getBlackHoleCollectionPublicPath())
457    }
458
459    /// Check if is the address a valid BlackHole address
460    ///
461    access(all)
462    view fun hasValidBlackHoleCollection(_ addr: Address): Bool {
463        return self.borrowBlackHoleCollection(addr)?.isValid() == true
464    }
465
466    init() {
467        let identifier = "BlackHole_".concat(self.account.address.toString()).concat("_receiver")
468        self.storagePath = StoragePath(identifier: identifier)!
469
470        self.blackHoles = {}
471    }
472}
473