Smart Contract
BaitCoin
A.ed2202de80195438.BaitCoin
1import FungibleToken from 0xf233dcee88fe0abe
2import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
3import MetadataViews from 0x1d7e57aa55817448
4import ViewResolver from 0x1d7e57aa55817448
5// Note: USDF is an EVM bridged token at address 0x1e4aa0b87d10b141
6// We'll interact with it through the FungibleToken interface
7
8access(all) contract BaitCoin: FungibleToken {
9
10 // Token metadata
11 access(all) let name: String
12 access(all) let symbol: String
13 access(all) let decimals: UInt8
14 access(all) var logoUrl: String
15 access(all) var metadata: String
16
17 // Total supply tracking
18 access(all) var totalSupply: UFix64
19
20 // Storage paths
21 access(all) let VaultStoragePath: StoragePath
22 access(all) let VaultPublicPath: PublicPath
23 access(all) let ReceiverPublicPath: PublicPath
24 access(all) let MinterStoragePath: StoragePath
25 access(all) let USDCVaultStoragePath: StoragePath
26
27 // Events
28 access(all) event TokensInitialized(initialSupply: UFix64)
29 access(all) event USDFToBaitSwap(user: Address, usdfAmount: UFix64, baitAmount: UFix64)
30 access(all) event BaitToUSDFSwap(user: Address, baitAmount: UFix64, usdfAmount: UFix64)
31 access(all) event LogoUrlUpdated(newLogoUrl: String)
32 access(all) event MetadataUpdated(newMetadata: String)
33
34 // Minter resource for minting tokens
35 access(all) resource Minter {
36 access(all) fun mintTokens(amount: UFix64): @{FungibleToken.Vault} {
37 BaitCoin.totalSupply = BaitCoin.totalSupply + amount
38 return <-create Vault(balance: amount)
39 }
40 }
41
42 // Admin resource for minting/burning and admin management
43 access(all) resource Admin {
44 access(all) fun mintBait(amount: UFix64, recipient: Address) {
45 BaitCoin.totalSupply = BaitCoin.totalSupply + amount
46
47 let recipientAccount = getAccount(recipient)
48 let receiver = recipientAccount.capabilities.get<&{FungibleToken.Receiver}>(BaitCoin.ReceiverPublicPath)
49 .borrow() ?? panic("Could not borrow receiver reference")
50
51 let tempVault <- create Vault(balance: amount)
52 receiver.deposit(from: <-tempVault)
53 }
54
55 access(all) fun burnBait(amount: UFix64, from: Address) {
56 // Note: This function requires the transaction to have proper authorization
57 // to withdraw from the target account. The actual burning should be done
58 // in the transaction that calls this function.
59 panic("This function should be called from a transaction with proper authorization")
60 }
61
62 access(all) fun setLogoUrl(newLogoUrl: String) {
63 BaitCoin.logoUrl = newLogoUrl
64 emit LogoUrlUpdated(newLogoUrl: newLogoUrl)
65 }
66
67 access(all) fun setMetadata(newMetadata: String) {
68 BaitCoin.metadata = newMetadata
69 emit MetadataUpdated(newMetadata: newMetadata)
70 }
71 }
72
73 // Admin management resource
74 access(all) resource AdminManager {
75 // Note: These functions require proper authorization and should be called from transactions
76 // that have the necessary capabilities
77 access(all) fun addAdmin(adminAddress: Address, adminCapability: Capability<&BaitCoin.Admin>) {
78 // This function should be called from a transaction with proper authorization
79 // The transaction signer must have the capability to publish at the target account
80 panic("This function should be called from a transaction with proper authorization")
81 }
82
83 access(all) fun removeAdmin(adminAddress: Address) {
84 // This function should be called from a transaction with proper authorization
85 // The transaction signer must have the capability to unpublish at the target account
86 panic("This function should be called from a transaction with proper authorization")
87 }
88 }
89
90 // Main vault resource
91 access(all) resource Vault: FungibleToken.Vault, ViewResolver.Resolver {
92 access(all) var balance: UFix64
93
94 init(balance: UFix64) {
95 self.balance = balance
96 }
97
98 access(all) view fun getViews(): [Type] {
99 return [
100 Type<FungibleTokenMetadataViews.FTDisplay>(),
101 Type<FungibleTokenMetadataViews.FTVaultData>()
102 ]
103 }
104
105 access(all) fun resolveView(_ view: Type): AnyStruct? {
106 switch view {
107 case Type<FungibleTokenMetadataViews.FTDisplay>():
108 let media = MetadataViews.Media(
109 file: MetadataViews.HTTPFile(url: BaitCoin.logoUrl),
110 mediaType: "image/png"
111 )
112 return FungibleTokenMetadataViews.FTDisplay(
113 name: BaitCoin.name,
114 symbol: BaitCoin.symbol,
115 description: BaitCoin.metadata,
116 externalURL: MetadataViews.ExternalURL("https://derby.fish"),
117 logos: MetadataViews.Medias([media]),
118 socials: {
119 "website": MetadataViews.ExternalURL("https://derby.fish/bait-coin-logo.png"),
120 "twitter": MetadataViews.ExternalURL("https://twitter.com/derby_fish")
121 }
122 )
123 case Type<FungibleTokenMetadataViews.FTVaultData>():
124 return FungibleTokenMetadataViews.FTVaultData(
125 storagePath: BaitCoin.VaultStoragePath,
126 receiverPath: BaitCoin.ReceiverPublicPath,
127 metadataPath: BaitCoin.VaultPublicPath,
128 receiverLinkedType: Type<&BaitCoin.Vault>(),
129 metadataLinkedType: Type<&BaitCoin.Vault>(),
130 createEmptyVaultFunction: (fun(): @{FungibleToken.Vault} {
131 return <-BaitCoin.createEmptyVault(vaultType: Type<@BaitCoin.Vault>())
132 })
133 )
134 }
135 return nil
136 }
137
138 access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
139 return {Type<@BaitCoin.Vault>(): true}
140 }
141
142 access(all) view fun isSupportedVaultType(type: Type): Bool {
143 return type == Type<@BaitCoin.Vault>()
144 }
145
146 access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool {
147 return amount <= self.balance
148 }
149
150 access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @BaitCoin.Vault {
151 pre {
152 amount <= self.balance: "Amount withdrawn must be less than or equal to the balance of the Vault"
153 }
154 self.balance = self.balance - amount
155 return <-create Vault(balance: amount)
156 }
157
158 access(all) fun deposit(from: @{FungibleToken.Vault}) {
159 let vault <- from as! @BaitCoin.Vault
160 self.balance = self.balance + vault.balance
161 vault.balance = 0.0
162 destroy vault
163 }
164
165 access(all) fun createEmptyVault(): @{FungibleToken.Vault} {
166 return <-create Vault(balance: 0.0)
167 }
168
169 access(all) fun createEmptyVaultWithType(vaultType: Type): @{FungibleToken.Vault} {
170 pre {
171 vaultType == Type<@BaitCoin.Vault>(): "Vault type mismatch"
172 }
173 return <-create Vault(balance: 0.0)
174 }
175
176
177 }
178
179 access(all) fun swapUSDFToBait(usdfVault: @{FungibleToken.Vault}, userAddress: Address): @{FungibleToken.Vault} {
180 let usdfAmount = (usdfVault).balance
181
182 if usdfAmount <= 0.0 {
183 panic("Amount must be greater than zero")
184 }
185
186 // Get the user's BAIT vault
187 let userAccount = getAccount(userAddress)
188 log("Attempting to get BAIT receiver for user: ".concat(userAddress.toString()))
189 log("Looking for receiver at path: ".concat(BaitCoin.ReceiverPublicPath.toString()))
190
191 let receiverCapability = userAccount.capabilities.get<&{FungibleToken.Receiver}>(BaitCoin.ReceiverPublicPath)
192 if receiverCapability != nil {
193 log("Receiver capability found: true")
194 } else {
195 log("Receiver capability found: false")
196 }
197
198 let baitReceiver = receiverCapability
199 .borrow() ?? panic("Could not borrow BAIT receiver reference. Please run setup_vault.cdc first to create your BAIT vault.")
200
201 // Deposit USDF to contract's vault for future BAIT to USDF swaps
202 // Store the USDF in the original EVM vault path
203 let contractUSDFVault = BaitCoin.account.storage.borrow<&{FungibleToken.Vault}>(from: /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault)
204 if contractUSDFVault == nil {
205 // Create the USDF vault if it doesn't exist by saving the incoming vault
206 BaitCoin.account.storage.save(<-usdfVault, to: /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault)
207 } else {
208 // Deposit to existing vault
209 contractUSDFVault!.deposit(from: <-usdfVault)
210 }
211
212 // Mint equivalent amount of BAIT
213 BaitCoin.totalSupply = BaitCoin.totalSupply + usdfAmount
214 let baitVault <- create Vault(balance: usdfAmount)
215
216 // Send BAIT to user
217 baitReceiver.deposit(from: <-baitVault)
218
219 emit USDFToBaitSwap(user: userAddress, usdfAmount: usdfAmount, baitAmount: usdfAmount)
220
221 // Return empty vault for transaction completion
222 return <-create Vault(balance: 0.0)
223 }
224
225 access(all) fun swapBaitToUSDF(baitVault: @{FungibleToken.Vault}, userAddress: Address): @{FungibleToken.Vault} {
226 let baitAmount = (baitVault).balance
227
228 if baitAmount <= 0.0 {
229 panic("Amount must be greater than zero")
230 }
231
232 // Get the user's USDF vault
233 let userAccount = getAccount(userAddress)
234 let usdfReceiverCapability = userAccount.capabilities.get<&{FungibleToken.Receiver}>(/public/usdfReceiver)
235 let usdfReceiver = usdfReceiverCapability
236 .borrow() ?? panic("Could not borrow USDF receiver reference. Please run createAllVault.cdc first to create your USDF vault.")
237
238 // Burn the BAIT tokens (reduce total supply)
239 BaitCoin.totalSupply = BaitCoin.totalSupply - baitAmount
240 destroy baitVault
241
242 // Withdraw equivalent USDF from contract's vault
243 let contractUSDFVault = BaitCoin.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(from: /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault)
244 ?? panic("Could not borrow contract USDF vault")
245
246 let usdfVault <- contractUSDFVault.withdraw(amount: baitAmount)
247
248 // Send USDF to user
249 usdfReceiver.deposit(from: <-usdfVault)
250
251 emit BaitToUSDFSwap(user: userAddress, baitAmount: baitAmount, usdfAmount: baitAmount)
252
253 // Return empty vault for transaction completion
254 return <-create Vault(balance: 0.0)
255 }
256
257
258 access(all) fun getTokenInfo(): {String: String} {
259 return {
260 "name": self.name,
261 "symbol": self.symbol,
262 "logoUrl": self.logoUrl,
263 "metadata": self.metadata,
264 "totalSupply": self.totalSupply.toString(),
265 "decimals": self.decimals.toString()
266 }
267 }
268
269 // FungibleTokenMetadataViews.Resolver implementation
270 access(all) fun getViews(): [Type] {
271 return [Type<FungibleTokenMetadataViews.FTView>()]
272 }
273
274 access(all) view fun getContractViews(resourceType: Type?): [Type] {
275 return [
276 Type<FungibleTokenMetadataViews.FTDisplay>(),
277 Type<FungibleTokenMetadataViews.FTVaultData>(),
278 Type<FungibleTokenMetadataViews.TotalSupply>()
279 ]
280 }
281
282 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
283 switch viewType {
284 case Type<FungibleTokenMetadataViews.FTDisplay>():
285 let media = MetadataViews.Media(
286 file: MetadataViews.HTTPFile(url: self.logoUrl),
287 mediaType: "image/png"
288 )
289 return FungibleTokenMetadataViews.FTDisplay(
290 name: self.name,
291 symbol: self.symbol,
292 description: self.metadata,
293 externalURL: MetadataViews.ExternalURL("https://derby.fish"),
294 logos: MetadataViews.Medias([media]),
295 socials: {
296 "website": MetadataViews.ExternalURL("https://derby.fish/bait-coin-logo.png"),
297 "twitter": MetadataViews.ExternalURL("https://twitter.com/derby_fish")
298 }
299 )
300 case Type<FungibleTokenMetadataViews.FTVaultData>():
301 return FungibleTokenMetadataViews.FTVaultData(
302 storagePath: self.VaultStoragePath,
303 receiverPath: self.ReceiverPublicPath,
304 metadataPath: self.VaultPublicPath,
305 receiverLinkedType: Type<&BaitCoin.Vault>(),
306 metadataLinkedType: Type<&BaitCoin.Vault>(),
307 createEmptyVaultFunction: (fun(): @{FungibleToken.Vault} {
308 return <-BaitCoin.createEmptyVault(vaultType: Type<@BaitCoin.Vault>())
309 })
310 )
311 case Type<FungibleTokenMetadataViews.TotalSupply>():
312 return FungibleTokenMetadataViews.TotalSupply(totalSupply: self.totalSupply)
313 }
314 return nil
315 }
316
317 access(all) fun getSupportedVaultTypes(): [Type] {
318 return [Type<@BaitCoin.Vault>()]
319 }
320
321 access(all) fun createEmptyVault(vaultType: Type): @BaitCoin.Vault {
322 return <-create Vault(balance: 0.0)
323 }
324
325 // Admin function to burn tokens and reduce total supply
326 access(all) fun burnTokens(amount: UFix64) {
327 self.totalSupply = self.totalSupply - amount
328 }
329
330 // Admin function to withdraw USDF from contract
331 access(all) fun withdrawUSDF(amount: UFix64, recipient: Address) {
332 let contractUSDFVault = BaitCoin.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(from: /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault)
333 ?? panic("Could not borrow contract USDF vault")
334
335 let usdfVault <- contractUSDFVault.withdraw(amount: amount)
336
337 let recipientAccount = getAccount(recipient)
338 let receiver = recipientAccount.capabilities.get<&{FungibleToken.Receiver}>(/public/usdfReceiver)
339 .borrow() ?? panic("Could not borrow recipient's USDF receiver reference")
340
341 receiver.deposit(from: <-usdfVault)
342 }
343
344 // Initialize the contract
345 init() {
346 self.name = "BAIT Coin"
347 self.symbol = "BAIT"
348 self.decimals = 8
349 self.logoUrl = "https://derby.fish/bait-coin-logo.png"
350 self.metadata = "BAIT COIN - A 1:1 pegged USDF token for the DerbyFish (https://derby.fish) ecosystem."
351 self.totalSupply = 0.0
352
353 // Set storage paths
354 self.VaultStoragePath = /storage/baitCoinVault
355 self.VaultPublicPath = /public/baitCoinVault
356 self.ReceiverPublicPath = /public/baitCoinReceiver
357 self.MinterStoragePath = /storage/baitCoinMinter
358 self.USDCVaultStoragePath = /storage/baitCoinUSDCVault
359
360 // Create and store the minter resource
361 let minter <- create Minter()
362 self.account.storage.save(<-minter, to: self.MinterStoragePath)
363
364 // Create and store the admin resource
365 let admin <- create Admin()
366 self.account.storage.save(<-admin, to: /storage/baitCoinAdmin)
367 let adminCapability = self.account.capabilities.storage.issue<&BaitCoin.Admin>(/storage/baitCoinAdmin)
368 self.account.capabilities.publish(adminCapability, at: /public/baitCoinAdmin)
369
370 // Create and store the admin manager resource
371 let adminManager <- create AdminManager()
372 self.account.storage.save(<-adminManager, to: /storage/baitCoinAdminManager)
373 let adminManagerCapability = self.account.capabilities.storage.issue<&BaitCoin.AdminManager>(/storage/baitCoinAdminManager)
374 self.account.capabilities.publish(adminManagerCapability, at: /public/baitCoinAdminManager)
375
376 // Note: USDF vault will be created when first USDF tokens are received
377 // The vault will be created dynamically in the swap functions
378
379 emit TokensInitialized(initialSupply: self.totalSupply)
380 }
381}