From baf710d26faa0c549ca969b5274e44a0f4bd2d5a Mon Sep 17 00:00:00 2001 From: Austin Kline Date: Thu, 26 Oct 2023 08:37:01 -0700 Subject: [PATCH] add FlowToken (#11) --- add.js | 48 +++- contracts/FlowToken.cdc | 274 +++++++++++++++++++++++ contracts/FungibleTokenMetadataViews.cdc | 183 +++++++++++++++ example/contracts/Importer.cdc | 1 + example/package.json | 1 - flow.json | 26 ++- index.js | 4 +- 7 files changed, 529 insertions(+), 8 deletions(-) create mode 100644 contracts/FlowToken.cdc create mode 100644 contracts/FungibleTokenMetadataViews.cdc diff --git a/add.js b/add.js index c40f4fe..e0042b0 100644 --- a/add.js +++ b/add.js @@ -3,18 +3,24 @@ const {getImports} = require("./dependency-tree"); const specialContractsHandlers = { "FungibleToken": (contract, userConfig, account) => { - console.log("FungibleToken requires some special setup. The account `emulator-ft` " + + return handleFungibleTokenAccountContract(contract, userConfig, account, "FungibleToken") + }, + "FungibleTokenMetadataViews": (contract, userConfig, account) => { + return handleFungibleTokenAccountContract(contract, userConfig, account, "FungibleTokenMetadataViews") + }, + "FlowToken": (contract, userConfig, account) => { + console.log("FlowToken requires some special setup. The account `emulator-flowtoken` " + "will be created and the contract will be deployed to it on the emulator. \nGoing forward, any deployments to the " + "flow emulator will require the --update flag to work correctly.") - const name = "FungibleToken" + const name = "FlowToken" const serverPK = userConfig.accounts[account].key const ftAccount = { - address: "ee82856bf20e2aa6", // this is the FungibleToken address on the flow emulator + address: "0ae53cb6e3f42a79", // this is the FungibleToken address on the flow emulator key: serverPK } - const emulatorAcct = "emulator-ft" + const emulatorAcct = "emulator-flowtoken" // ensure emulator-ft is an account userConfig.accounts[emulatorAcct] = ftAccount @@ -131,6 +137,40 @@ const addAll = (path, account) => { }) } +const handleFungibleTokenAccountContract = (contract, userConfig, account, contractName) => { + console.log(`FungibleToken requires some special setup. The account "emulator-ft"\n + will be created and the contract will be deployed to it on the emulator. \nGoing forward, any deployments to the\n + flow emulator will require the --update flag to work correctly.`) + + const serverPK = userConfig.accounts[account].key + const ftAccount = { + address: "ee82856bf20e2aa6", // this is the FungibleToken address on the flow emulator + key: serverPK + } + const emulatorAcct = "emulator-ft" + + // ensure emulator-ft is an account + userConfig.accounts[emulatorAcct] = ftAccount + if (!userConfig.deployments) { + userConfig.deployments = {} + } + + // ensure that emulator-ft is a deployment account + if (!userConfig.deployments.emulator) { + userConfig.deployments.emulator = {} + } + + if (!userConfig.deployments.emulator[emulatorAcct]) { + userConfig.deployments.emulator[emulatorAcct] = [] + } + + userConfig.contracts[contractName] = contract + + if (!userConfig.deployments.emulator[emulatorAcct].includes(contractName)) { + userConfig.deployments.emulator[emulatorAcct].push(contractName) + } +} + module.exports = { add, addAll diff --git a/contracts/FlowToken.cdc b/contracts/FlowToken.cdc new file mode 100644 index 0000000..664d853 --- /dev/null +++ b/contracts/FlowToken.cdc @@ -0,0 +1,274 @@ +import "FungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +pub contract FlowToken: FungibleToken, ViewResolver { + + // Total supply of Flow tokens in existence + pub var totalSupply: UFix64 + + // Event that is emitted when the contract is created + pub event TokensInitialized(initialSupply: UFix64) + + // Event that is emitted when tokens are withdrawn from a Vault + pub event TokensWithdrawn(amount: UFix64, from: Address?) + + // Event that is emitted when tokens are deposited to a Vault + pub event TokensDeposited(amount: UFix64, to: Address?) + + // Event that is emitted when new tokens are minted + pub event TokensMinted(amount: UFix64) + + // Event that is emitted when tokens are destroyed + pub event TokensBurned(amount: UFix64) + + // Event that is emitted when a new minter resource is created + pub event MinterCreated(allowedAmount: UFix64) + + // Event that is emitted when a new burner resource is created + pub event BurnerCreated() + + // Vault + // + // Each user stores an instance of only the Vault in their storage + // The functions in the Vault and governed by the pre and post conditions + // in FungibleToken when they are called. + // The checks happen at runtime whenever a function is called. + // + // Resources can only be created in the context of the contract that they + // are defined in, so there is no way for a malicious user to create Vaults + // out of thin air. A special Minter resource needs to be defined to mint + // new tokens. + // + pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance, MetadataViews.Resolver { + + // holds the balance of a users tokens + pub var balance: UFix64 + + // initialize the balance at resource creation time + init(balance: UFix64) { + self.balance = balance + } + + // withdraw + // + // Function that takes an integer amount as an argument + // and withdraws that amount from the Vault. + // It creates a new temporary Vault that is used to hold + // the money that is being transferred. It returns the newly + // created Vault to the context that called so it can be deposited + // elsewhere. + // + pub fun withdraw(amount: UFix64): @FungibleToken.Vault { + self.balance = self.balance - amount + emit TokensWithdrawn(amount: amount, from: self.owner?.address) + return <-create Vault(balance: amount) + } + + // deposit + // + // Function that takes a Vault object as an argument and adds + // its balance to the balance of the owners Vault. + // It is allowed to destroy the sent Vault because the Vault + // was a temporary holder of the tokens. The Vault's balance has + // been consumed and therefore can be destroyed. + pub fun deposit(from: @FungibleToken.Vault) { + let vault <- from as! @FlowToken.Vault + self.balance = self.balance + vault.balance + emit TokensDeposited(amount: vault.balance, to: self.owner?.address) + vault.balance = 0.0 + destroy vault + } + + destroy() { + if self.balance > 0.0 { + FlowToken.totalSupply = FlowToken.totalSupply - self.balance + } + } + + /// Get all the Metadata Views implemented by FlowToken + /// + /// @return An array of Types defining the implemented views. This value will be used by + /// developers to know which parameter to pass to the resolveView() method. + /// + pub fun getViews(): [Type]{ + return FlowToken.getViews() + } + + /// Get a Metadata View from FlowToken + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + pub fun resolveView(_ view: Type): AnyStruct? { + return FlowToken.resolveView(view) + } + } + + // createEmptyVault + // + // Function that creates a new Vault with a balance of zero + // and returns it to the calling context. A user must call this function + // and store the returned Vault in their storage in order to allow their + // account to be able to receive deposits of this token type. + // + pub fun createEmptyVault(): @FungibleToken.Vault { + return <-create Vault(balance: 0.0) + } + + pub fun getViews(): [Type] { + return [Type(), + Type(), + Type()] + } + + /// Get a Metadata View from FlowToken + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + pub fun resolveView(_ view: Type): AnyStruct? { + switch view { + case Type(): + return FungibleTokenMetadataViews.FTView( + ftDisplay: self.resolveView(Type()) as! FungibleTokenMetadataViews.FTDisplay?, + ftVaultData: self.resolveView(Type()) as! FungibleTokenMetadataViews.FTVaultData? + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + ), + mediaType: "image/svg+xml" + ) + let medias = MetadataViews.Medias([media]) + return FungibleTokenMetadataViews.FTDisplay( + name: "FLOW Network Token", + symbol: "FLOW", + description: "FLOW is the protocol token that is required for transaction fees, storage fees, staking, and many applications built on the Flow Blockchain", + externalURL: MetadataViews.ExternalURL("https://flow.com"), + logos: medias, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + case Type(): + return FungibleTokenMetadataViews.FTVaultData( + storagePath: /storage/flowTokenVault, + receiverPath: /public/flowTokenReceiver, + metadataPath: /public/flowTokenBalance, + providerPath: /private/flowTokenVault, + receiverLinkedType: Type<&FlowToken.Vault{FungibleToken.Receiver, FungibleToken.Balance, MetadataViews.Resolver}>(), + metadataLinkedType: Type<&FlowToken.Vault{FungibleToken.Balance, MetadataViews.Resolver}>(), + providerLinkedType: Type<&FlowToken.Vault{FungibleToken.Provider}>(), + createEmptyVaultFunction: (fun (): @FungibleToken.Vault { + return <-FlowToken.createEmptyVault() + }) + ) + } + return nil + } + + pub resource Administrator { + // createNewMinter + // + // Function that creates and returns a new minter resource + // + pub fun createNewMinter(allowedAmount: UFix64): @Minter { + emit MinterCreated(allowedAmount: allowedAmount) + return <-create Minter(allowedAmount: allowedAmount) + } + + // createNewBurner + // + // Function that creates and returns a new burner resource + // + pub fun createNewBurner(): @Burner { + emit BurnerCreated() + return <-create Burner() + } + } + + // Minter + // + // Resource object that token admin accounts can hold to mint new tokens. + // + pub resource Minter { + + // the amount of tokens that the minter is allowed to mint + pub var allowedAmount: UFix64 + + // mintTokens + // + // Function that mints new tokens, adds them to the total supply, + // and returns them to the calling context. + // + pub fun mintTokens(amount: UFix64): @FlowToken.Vault { + pre { + amount > UFix64(0): "Amount minted must be greater than zero" + amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" + } + FlowToken.totalSupply = FlowToken.totalSupply + amount + self.allowedAmount = self.allowedAmount - amount + emit TokensMinted(amount: amount) + return <-create Vault(balance: amount) + } + + init(allowedAmount: UFix64) { + self.allowedAmount = allowedAmount + } + } + + // Burner + // + // Resource object that token admin accounts can hold to burn tokens. + // + pub resource Burner { + + // burnTokens + // + // Function that destroys a Vault instance, effectively burning the tokens. + // + // Note: the burned tokens are automatically subtracted from the + // total supply in the Vault destructor. + // + pub fun burnTokens(from: @FungibleToken.Vault) { + let vault <- from as! @FlowToken.Vault + let amount = vault.balance + destroy vault + emit TokensBurned(amount: amount) + } + } + + init(adminAccount: AuthAccount) { + self.totalSupply = 0.0 + + // Create the Vault with the total supply of tokens and save it in storage + // + let vault <- create Vault(balance: self.totalSupply) + adminAccount.save(<-vault, to: /storage/flowTokenVault) + + // Create a public capability to the stored Vault that only exposes + // the `deposit` method through the `Receiver` interface + // + adminAccount.link<&FlowToken.Vault{FungibleToken.Receiver, FungibleToken.Balance, MetadataViews.Resolver}>( + /public/flowTokenReceiver, + target: /storage/flowTokenVault + ) + + // Create a public capability to the stored Vault that only exposes + // the `balance` field through the `Balance` interface + // + adminAccount.link<&FlowToken.Vault{FungibleToken.Balance, MetadataViews.Resolver}>( + /public/flowTokenBalance, + target: /storage/flowTokenVault + ) + + let admin <- create Administrator() + adminAccount.save(<-admin, to: /storage/flowTokenAdmin) + + // Emit an event that shows that the contract was initialized + emit TokensInitialized(initialSupply: self.totalSupply) + } +} diff --git a/contracts/FungibleTokenMetadataViews.cdc b/contracts/FungibleTokenMetadataViews.cdc new file mode 100644 index 0000000..f2b470b --- /dev/null +++ b/contracts/FungibleTokenMetadataViews.cdc @@ -0,0 +1,183 @@ +import "FungibleToken" +import "MetadataViews" + +/// This contract implements the metadata standard proposed +/// in FLIP-1087. +/// +/// Ref: https://github.com/onflow/flow/blob/master/flips/20220811-fungible-tokens-metadata.md +/// +/// Structs and resources can implement one or more +/// metadata types, called views. Each view type represents +/// a different kind of metadata. +/// +pub contract FungibleTokenMetadataViews { + /// FTView wraps FTDisplay and FTVaultData, and is used to give a complete + /// picture of a Fungible Token. Most Fungible Token contracts should + /// implement this view. + /// + pub struct FTView { + pub let ftDisplay: FTDisplay? + pub let ftVaultData: FTVaultData? + init( + ftDisplay: FTDisplay?, + ftVaultData: FTVaultData? + ) { + self.ftDisplay = ftDisplay + self.ftVaultData = ftVaultData + } + } + + /// Helper to get a FT view. + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A FTView struct + /// + pub fun getFTView(viewResolver: &{MetadataViews.Resolver}): FTView { + let maybeFTView = viewResolver.resolveView(Type()) + if let ftView = maybeFTView { + return ftView as! FTView + } + return FTView( + ftDisplay: self.getFTDisplay(viewResolver), + ftVaultData: self.getFTVaultData(viewResolver) + ) + } + + /// View to expose the information needed to showcase this FT. + /// This can be used by applications to give an overview and + /// graphics of the FT. + /// + pub struct FTDisplay { + /// The display name for this token. + /// + /// Example: "Flow" + /// + pub let name: String + + /// The abbreviated symbol for this token. + /// + /// Example: "FLOW" + pub let symbol: String + + /// A description the provides an overview of this token. + /// + /// Example: "The FLOW token is the native currency of the Flow network." + pub let description: String + + /// External link to a URL to view more information about the fungible token. + pub let externalURL: MetadataViews.ExternalURL + + /// One or more versions of the fungible token logo. + pub let logos: MetadataViews.Medias + + /// Social links to reach the fungible token's social homepages. + /// Possible keys may be "instagram", "twitter", "discord", etc. + pub let socials: {String: MetadataViews.ExternalURL} + + init( + name: String, + symbol: String, + description: String, + externalURL: MetadataViews.ExternalURL, + logos: MetadataViews.Medias, + socials: {String: MetadataViews.ExternalURL} + ) { + self.name = name + self.symbol = symbol + self.description = description + self.externalURL = externalURL + self.logos = logos + self.socials = socials + } + } + + /// Helper to get FTDisplay in a way that will return a typed optional. + /// + /// @param viewResolver: A reference to the resolver resource + /// @return An optional FTDisplay struct + /// + pub fun getFTDisplay(_ viewResolver: &{MetadataViews.Resolver}): FTDisplay? { + if let maybeDisplayView = viewResolver.resolveView(Type()) { + if let displayView = maybeDisplayView as? FTDisplay { + return displayView + } + } + return nil + } + + /// View to expose the information needed store and interact with a FT vault. + /// This can be used by applications to setup a FT vault with proper + /// storage and public capabilities. + /// + pub struct FTVaultData { + /// Path in storage where this FT vault is recommended to be stored. + pub let storagePath: StoragePath + + /// Public path which must be linked to expose the public receiver capability. + pub let receiverPath: PublicPath + + /// Public path which must be linked to expose the balance and resolver public capabilities. + pub let metadataPath: PublicPath + + /// Private path which should be linked to expose the provider capability to withdraw funds + /// from the vault. + pub let providerPath: PrivatePath + + /// Type that should be linked at the `receiverPath`. This is a restricted type requiring + /// the `FungibleToken.Receiver` interface. + pub let receiverLinkedType: Type + + /// Type that should be linked at the `receiverPath`. This is a restricted type requiring + /// the `FungibleToken.Balance` and `MetadataViews.Resolver` interfaces. + pub let metadataLinkedType: Type + + /// Type that should be linked at the aforementioned private path. This + /// is normally a restricted type with at a minimum the `FungibleToken.Provider` interface. + pub let providerLinkedType: Type + + /// Function that allows creation of an empty FT vault that is intended + /// to store the funds. + pub let createEmptyVault: ((): @FungibleToken.Vault) + + init( + storagePath: StoragePath, + receiverPath: PublicPath, + metadataPath: PublicPath, + providerPath: PrivatePath, + receiverLinkedType: Type, + metadataLinkedType: Type, + providerLinkedType: Type, + createEmptyVaultFunction: ((): @FungibleToken.Vault) + ) { + pre { + receiverLinkedType.isSubtype(of: Type<&{FungibleToken.Receiver}>()): "Receiver public type must include FungibleToken.Receiver." + metadataLinkedType.isSubtype(of: Type<&{FungibleToken.Balance, MetadataViews.Resolver}>()): "Metadata public type must include FungibleToken.Balance and MetadataViews.Resolver interfaces." + providerLinkedType.isSubtype(of: Type<&{FungibleToken.Provider}>()): "Provider type must include FungibleToken.Provider interface." + } + self.storagePath = storagePath + self.receiverPath = receiverPath + self.metadataPath = metadataPath + self.providerPath = providerPath + self.receiverLinkedType = receiverLinkedType + self.metadataLinkedType = metadataLinkedType + self.providerLinkedType = providerLinkedType + self.createEmptyVault = createEmptyVaultFunction + } + } + + /// Helper to get FTVaultData in a way that will return a typed Optional. + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional FTVaultData struct + /// + pub fun getFTVaultData(_ viewResolver: &{MetadataViews.Resolver}): FTVaultData? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? FTVaultData { + return v + } + } + return nil + } + +} + \ No newline at end of file diff --git a/example/contracts/Importer.cdc b/example/contracts/Importer.cdc index 40fc518..97eda64 100644 --- a/example/contracts/Importer.cdc +++ b/example/contracts/Importer.cdc @@ -5,6 +5,7 @@ import "ViewResolver" import "HybridCustody" import "NFTCatalog" import "NFTCatalogAdmin" +import "FlowToken" // This contract doesn't do anything, it's just to show that deployments work // with this import system diff --git a/example/package.json b/example/package.json index e668636..2528764 100644 --- a/example/package.json +++ b/example/package.json @@ -9,7 +9,6 @@ "author": "", "license": "ISC", "dependencies": { - "@flowty/flow-contracts": "file:..", "@flowtyio/flow-contracts": "file:.." } } diff --git a/flow.json b/flow.json index 86c2934..9ce2bf9 100644 --- a/flow.json +++ b/flow.json @@ -13,6 +13,10 @@ "emulator-ft": { "address": "ee82856bf20e2aa6", "key": "a8201e155882e2a7ec94644ef0f023ecce8baec418276f95217db1ecf90b03db" + }, + "emulator-flowtoken": { + "address": "0ae53cb6e3f42a79", + "key": "a8201e155882e2a7ec94644ef0f023ecce8baec418276f95217db1ecf90b03db" } }, "contracts": { @@ -40,6 +44,14 @@ "mainnet": "0xf233dcee88fe0abe" } }, + "FungibleTokenMetadataViews": { + "source": "./contracts/FungibleTokenMetadataViews.cdc", + "aliases": { + "emulator": "0xee82856bf20e2aa6", + "testnet": "0x9a0766d93b6608b7", + "mainnet": "0xf233dcee88fe0abe" + } + }, "ViewResolver": { "source": "./contracts/ViewResolver.cdc", "aliases": { @@ -151,6 +163,14 @@ "testnet": "0x324c34e1c517e4db", "mainnet": "0x49a7cda3a1eecc29" } + }, + "FlowToken": { + "source": "./contracts/FlowToken.cdc", + "aliases": { + "emulator": "0x0ae53cb6e3f42a79", + "testnet": "0x7e60df042a9c0868", + "mainnet": "0x1654653399040a61" + } } }, "deployments": { @@ -163,7 +183,11 @@ "NFTCatalogAdmin" ], "emulator-ft": [ - "FungibleToken" + "FungibleToken", + "FungibleTokenMetadataViews" + ], + "emulator-flowtoken": [ + "FlowToken" ] } } diff --git a/index.js b/index.js index b9cad47..2e1494b 100755 --- a/index.js +++ b/index.js @@ -33,8 +33,8 @@ program.command("add-all") .option('-a, --account ', 'Account to be used for signing', 'emulator-account') .action(({config, account}) => { if(!config) { - options.config = getDefaultConfigPath() - console.log("no config specified, using default config: ", options.config) + config = getDefaultConfigPath() + console.log("no config specified, using default config: ", config) } addAll(config, account)