Smart Contract

FRC20Converter

A.d2abb5dbf5e08666.FRC20Converter

Valid From

86,129,078

Deployed

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

Dependents

0 imports
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FRC20 Token Converter
5
6This contract is used to convert FRC20 tokens to Fungible Tokens and vice versa.
7
8*/
9import FungibleToken from 0xf233dcee88fe0abe
10import FlowToken from 0x1654653399040a61
11// Fixes Imports
12import Fixes from 0xd2abb5dbf5e08666
13import FixesInscriptionFactory from 0xd2abb5dbf5e08666
14import FixesFungibleTokenInterface from 0xd2abb5dbf5e08666
15import FRC20FTShared from 0xd2abb5dbf5e08666
16import FRC20Indexer from 0xd2abb5dbf5e08666
17
18access(all) contract FRC20Converter {
19    /* --- Events --- */
20
21    /// Event emitted when the contract is initialized
22    access(all) event ContractInitialized()
23    /// Event emitted when the FRC20 Converter is created
24    access(all) event FRC20ConverterCreated(
25        _ ticker: String,
26        tokenType: Type
27    )
28    /// Event emitted when the FRC20 tokens are burned
29    access(all) event FRC20TokenBurned(
30        _ ticker: String,
31        amount: UFix64,
32        flowRefund: UFix64,
33        receiver: Type
34    )
35
36    /** --- Interfaces & Resources --- */
37
38    /// Interface for the FRC20 Treasury Receiver
39    ///
40    access(all) resource interface FRC20TreasuryReceiver {
41        access(all)
42        fun depositFlowToken(_ token: @{FungibleToken.Vault}) {
43            pre {
44                token.isInstance(Type<@FlowToken.Vault>()): "Invalid token type"
45            }
46        }
47    }
48
49    /// Interface for the FRC20 Burner
50    ///
51    access(all) resource interface IFRC20Burner {
52        /// check if the ticker is burnable
53        access(all)
54        view fun isTickerBurnable(_ tick: String): Bool
55        /// all the unburnable ticks
56        access(all)
57        view fun getUnburnableTicks(): [String]
58        /// burn and send the tokens
59        access(all)
60        fun burnAndSend(_ ins: auth(Fixes.Extractable) &Fixes.Inscription, recipient: &{FRC20TreasuryReceiver})
61    }
62
63    /// System Burner
64    ///
65    access(all) resource SystemBurner: IFRC20Burner {
66        /// all the unburnable ticks
67        ///
68        access(all)
69        view fun getUnburnableTicks(): [String] {
70            return [
71                FRC20FTShared.getPlatformStakingTickerName(),
72                FRC20FTShared.getPlatformUtilityTickerName()
73            ]
74        }
75
76        /// check if the ticker is burnable
77        ///
78        access(all)
79        view fun isTickerBurnable(_ tick: String): Bool {
80            return !self.getUnburnableTicks().contains(tick)
81        }
82
83        /// burn and send the tokens
84        ///
85        access(all)
86        fun burnAndSend(_ ins: auth(Fixes.Extractable) &Fixes.Inscription, recipient: &{FRC20TreasuryReceiver}) {
87            pre {
88                ins.isExtractable(): "Inscription is not extractable"
89            }
90            post {
91                ins.isExtracted(): "Inscription is not extracted"
92            }
93            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
94            let tick = meta["tick"]?.toLower() ?? panic("The token tick is not found")
95            assert(
96                self.isTickerBurnable(tick),
97                message: "The token is not burnable by the system burner."
98            )
99            let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
100            // burn the tokens
101            let frc20Indexer = FRC20Indexer.getIndexer()
102            let flowVault <- frc20Indexer.burnFromTreasury(ins: ins)
103            let flowRefundAmount = flowVault.balance
104            // deposit the flow tokens
105            recipient.depositFlowToken(<- flowVault)
106
107            // emit the event
108            emit FRC20TokenBurned(
109                tick,
110                amount: amt,
111                flowRefund: flowRefundAmount,
112                receiver: recipient.getType()
113            )
114        }
115    }
116
117    /// Interface for the FRC20 Converter
118    ///
119    access(all) resource interface IConverter {
120        /// get the ticker name
121        access(all)
122        view fun getTickerName(): String
123        /// get the token type
124        access(all)
125        view fun getTokenType(): Type
126        /// convert the frc20 to the frc20 FT
127        access(all)
128        fun convertFromIndexer(ins: auth(Fixes.Extractable) &Fixes.Inscription, recipient: &{FungibleToken.Receiver}) {
129            pre {
130                ins.isExtractable(): "Inscription is not extractable"
131                recipient.getSupportedVaultTypes()[self.getTokenType()] == true: "Recipient does not support the token type"
132            }
133            post {
134                ins.isExtracted(): "Inscription is not extracted"
135            }
136        }
137        /// convert the frc20 FT to the frc20
138        access(all)
139        fun convertBackToIndexer(ins: auth(Fixes.Extractable) &Fixes.Inscription, vault: @{FungibleToken.Vault}) {
140            pre {
141                ins.isExtractable(): "Inscription is not extractable"
142                vault.getType() == self.getTokenType(): "Vault does not support the token type"
143            }
144            post {
145                ins.isExtracted(): "Inscription is not extracted"
146            }
147        }
148    }
149
150    /// The general FRC20 Converter for arbitrary FRC20 Token
151    ///
152    access(all) resource FTConverter: IConverter {
153        access(self)
154        let adminCap: Capability<auth(FixesFungibleTokenInterface.Manage) &{FixesFungibleTokenInterface.IAdminWritable}>
155
156        init(
157            _ cap: Capability<auth(FixesFungibleTokenInterface.Manage) &{FixesFungibleTokenInterface.IAdminWritable}>
158        ) {
159            self.adminCap = cap
160
161            // emit the event
162            let minter = self.borrowMinterRef()
163            emit FRC20ConverterCreated(
164                minter.getSymbol(),
165                tokenType: minter.getTokenType()
166            )
167        }
168
169        // ---- IConverter ----
170
171        /// get the ticker name
172        ///
173        access(all)
174        view fun getTickerName(): String {
175            let minter = self.borrowMinterRef()
176            return minter.getSymbol()
177        }
178
179        /// get the token type
180        access(all)
181        view fun getTokenType(): Type {
182            let minter = self.borrowMinterRef()
183            return minter.getTokenType()
184        }
185
186        /// convert the frc20 to the frc20 FT
187        ///
188        access(all)
189        fun convertFromIndexer(ins: auth(Fixes.Extractable) &Fixes.Inscription, recipient: &{FungibleToken.Receiver}) {
190            // borrow the minter reference
191            let minter = self.borrowMinterRef()
192
193            // parse the metadata
194            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
195            let tick = meta["tick"]?.toLower() ?? panic("The token tick is not found")
196            let minterTicker = minter.getSymbol()
197            assert(
198                minterTicker == tick,
199                message: "The token tick does not match the converter ticker"
200            )
201
202            let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
203            let minterType = minter.getTokenType()
204            // convert the tokens
205            let emptyVault <- minter.mintTokens(amount: 0.0)
206            let convertedVault <- minter.initializeVaultByInscription(vault: <- emptyVault, ins: ins)
207            assert(
208                convertedVault.getType() == minterType,
209                message: "The converted vault type does not match the minter type"
210            )
211            assert(
212                amt == convertedVault.balance,
213                message: "The converted vault balance does not match the inscription amount"
214            )
215
216            recipient.deposit(from: <- convertedVault)
217        }
218
219        /// convert the frc20 FT to the frc20
220        ///
221        access(all)
222        fun convertBackToIndexer(ins: auth(Fixes.Extractable) &Fixes.Inscription, vault: @{FungibleToken.Vault}) {
223            // borrow the minter reference
224            let minter = self.borrowMinterRef()
225
226            // parse the metadata
227            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
228            let tick = meta["tick"]?.toLower() ?? panic("The token tick is not found")
229            let minterTicker = minter.getSymbol()
230            assert(
231                minterTicker == tick,
232                message: "The token tick does not match the converter ticker"
233            )
234            // burn the tokens
235            minter.burnTokenWithInscription(vault: <- vault, ins: ins)
236        }
237
238        // ----- Internal Methods ----
239
240        /// Borrow the admin reference
241        ///
242        access(self)
243        view fun borrowAdminRef(): auth(FixesFungibleTokenInterface.Manage) &{FixesFungibleTokenInterface.IAdminWritable} {
244            return self.adminCap.borrow()
245                ?? panic("Could not borrow the admin reference")
246        }
247
248        /// Borrow the super minter reference
249        ///
250        access(self)
251        view fun borrowMinterRef(): auth(FixesFungibleTokenInterface.Manage) &{FixesFungibleTokenInterface.IMinter} {
252            return self.borrowAdminRef().borrowSuperMinter()
253        }
254    }
255
256    /** ------- Public Methods ---- */
257
258    /// Borrow the system burner reference
259    ///
260    access(all)
261    view fun borrowSystemBurner(): &SystemBurner {
262        return getAccount(self.account.address)
263            .capabilities.get<&SystemBurner>(self.getSystemBurnerPublicPath())
264            .borrow()
265            ?? panic("Could not borrow the SystemBurner reference")
266    }
267
268    /// Create the FRC20 Converter
269    ///
270    access(all)
271    fun createConverter(
272        _ privCap: Capability<auth(FixesFungibleTokenInterface.Manage) &{FixesFungibleTokenInterface.IAdminWritable}>
273    ): @FTConverter {
274        return <- create FTConverter(privCap)
275    }
276
277    /// Borrow the FRC20 Converter
278    ///
279    access(all)
280    view fun borrowConverter(_ addr: Address): &FTConverter? {
281        return getAccount(addr)
282            .capabilities.get<&FTConverter>(self.getFTConverterPublicPath())
283            .borrow()
284    }
285
286    /// Get the prefix for the storage paths
287    ///
288    access(all)
289    view fun getPathPrefix(): String {
290        return "FRC20Converter_".concat(self.account.address.toString()).concat("_")
291    }
292
293    /// Get the system burner storage path
294    ///
295    access(all)
296    view fun getSystemBurnerStoragePath(): StoragePath {
297        let prefix = self.getPathPrefix()
298        return StoragePath(identifier: prefix.concat("SystemBurner"))!
299    }
300
301    /// Get the system burner public path
302    ///
303    access(all)
304    view fun getSystemBurnerPublicPath(): PublicPath {
305        let prefix = self.getPathPrefix()
306        return PublicPath(identifier: prefix.concat("SystemBurner"))!
307    }
308
309    /// Get the ft converter storage path
310    ///
311    access(all)
312    view fun getFTConverterStoragePath(): StoragePath {
313        let prefix = self.getPathPrefix()
314        return StoragePath(identifier: prefix.concat("FTConverter"))!
315    }
316
317    /// Get the ft converter public path
318    ///
319    access(all)
320    view fun getFTConverterPublicPath(): PublicPath {
321        let prefix = self.getPathPrefix()
322        return PublicPath(identifier: prefix.concat("FTConverter"))!
323    }
324
325    init() {
326        let storagePath = self.getSystemBurnerStoragePath()
327        self.account.storage.save(<- create SystemBurner(), to: storagePath)
328        // publish the public path
329        self.account.capabilities.publish(
330            self.account.capabilities.storage.issue<&SystemBurner>(storagePath),
331            at: self.getSystemBurnerPublicPath()
332        )
333
334        emit ContractInitialized()
335    }
336}
337