diff --git a/contracts/ExampleTokens.cdc b/contracts/ExampleTokens.cdc new file mode 100644 index 00000000..dd4433da --- /dev/null +++ b/contracts/ExampleTokens.cdc @@ -0,0 +1,255 @@ +import FungibleTokens from "./FungibleTokens.cdc" // 0xFungibleTokensADDRESS + +pub contract ExampleTokens: FungibleTokens { + + /// Total supply of ExampleTokenSets in existence + access(contract) var totalSupplyByID: {UInt64: UFix64} + + pub var CollectionStoragePath: StoragePath + + pub event ContractInitialized() + + /// TokensInitialized + /// + /// The event that is emitted when a new token is created + pub event TokensInitialized(initialSupply: UFix64) + + /// TokensWithdrawn + /// + /// The event that is emitted when tokens are withdrawn from a Vault + pub event TokensWithdrawn(tokenID: UInt64, amount: UFix64, from: Address?) + + /// TokensDeposited + /// + /// The event that is emitted when tokens are deposited to a Vault + pub event TokensDeposited(tokenID: UInt64, amount: UFix64, to: Address?) + + /// TokensMinted + /// + /// The event that is emitted when new tokens are minted + pub event TokensMinted(tokenID: UInt64, amount: UFix64) + + /// TokensBurned + /// + /// The event that is emitted when tokens are destroyed + pub event TokensBurned(tokenID: UInt64, amount: UFix64) + + /// MinterCreated + /// + /// The event that is emitted when a new minter resource is created + pub event MinterCreated(tokenID: UInt64, allowedAmount: UFix64) + + /// BurnerCreated + /// + /// The 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 FungibleTokens 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 TokenVault: FungibleTokens.Provider, FungibleTokens.Receiver, FungibleTokens.Balance { + + /// The total balance of this vault + pub var balance: UFix64 + pub let tokenID: UInt64 + + // initialize the balance at resource creation time + init(tokenID: UInt64, balance: UFix64) { + self.balance = balance + self.tokenID = tokenID + } + + /// withdraw + /// + /// Function that takes an 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): @FungibleTokens.TokenVault { + self.balance = self.balance - amount + emit TokensWithdrawn(tokenID: self.tokenID, amount: amount, from: self.owner?.address) + return <-create TokenVault(tokenID: self.tokenID, 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: @FungibleTokens.TokenVault) { + let vault <- from as! @ExampleTokens.TokenVault + self.balance = self.balance + vault.balance + emit TokensDeposited(tokenID: self.tokenID, amount: vault.balance, to: self.owner?.address) + vault.balance = 0.0 + destroy vault + } + + destroy() { + ExampleTokens.totalSupplyByID[self.tokenID] = ExampleTokens.totalSupplyByID[self.tokenID]! - self.balance + } + } + + /// 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 createEmptyTokenVault(tokenID: UInt64): @TokenVault { + return <-create TokenVault(tokenID: tokenID, balance: 0.0) + } + + pub resource Administrator { + + /// createNewMinter + /// + /// Function that creates and returns a new minter resource + /// Minter can mint an allowance of tokens of the specified ID + /// + pub fun createNewMinter(tokenID: UInt64, allowedAmount: UFix64): @Minter { + emit MinterCreated(tokenID: tokenID, allowedAmount: allowedAmount) + return <-create Minter(tokenID: tokenID, 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 + pub var tokenID: UInt64 + + /// mintTokens + /// + /// Function that mints new tokens, adds them to the total supply, + /// and returns them to the calling context. + /// + pub fun mintTokens(amount: UFix64): @ExampleTokens.TokenVault { + pre { + amount > 0.0: "Amount minted must be greater than zero" + amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" + } + ExampleTokens.totalSupplyByID[self.tokenID] = ExampleTokens.totalSupplyByID[self.tokenID]! + amount + self.allowedAmount = self.allowedAmount - amount + emit TokensMinted(tokenID: self.tokenID, amount: amount) + return <-create TokenVault(tokenID: self.tokenID, balance: amount) + } + + init(tokenID: UInt64, allowedAmount: UFix64) { + self.allowedAmount = allowedAmount + self.tokenID = tokenID + } + } + + /// 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: @FungibleTokens.TokenVault) { + let vault <- from as! @ExampleTokens.TokenVault + let amount = vault.balance + let tokenID = vault.tokenID + destroy vault + emit TokensBurned(tokenID: tokenID, amount: amount) + } + } + + pub resource Collection: FungibleTokens.CollectionPublic, FungibleTokens.CollectionPrivate { + pub var ownedTokenVaults: @{UInt64: FungibleTokens.TokenVault} + + pub fun deposit(token: @FungibleTokens.TokenVault) { + if self.ownedTokenVaults[token.tokenID] == nil { + self.ownedTokenVaults[token.tokenID] <-! token + } else { + self.ownedTokenVaults[token.tokenID]?.deposit!(from: <- token) + } + } + + pub fun getIDs(): [UInt64] { + return self.ownedTokenVaults.keys + } + + pub fun borrowTokenVault(id: UInt64): &FungibleTokens.TokenVault { + let vaultRef = &self.ownedTokenVaults[id] as! &FungibleTokens.TokenVault + return vaultRef + } + + init() { + self.ownedTokenVaults <- {} + } + + destroy () { + destroy self.ownedTokenVaults + } + + } + + pub fun createEmptyCollection(): @FungibleTokens.Collection { + return <- create Collection() + } + + init() { + self.totalSupplyByID = {0: 1000.0} + self.CollectionStoragePath = /storage/ExampleTokens + + // Create the Vault with the total supply of tokens and save it in storage + // + let vault <- create TokenVault(tokenID: 0, balance: self.totalSupplyByID[0]!) + + // create collection to store the vaults + let collection <- ExampleTokens.createEmptyCollection() + + // deposit vault into collection + collection.deposit(token: <- vault) + + // save collection to storage + self.account.save(<-collection, to: self.CollectionStoragePath) + + // create admin and save to storage + let admin <- create Administrator() + self.account.save(<-admin, to: /storage/ExampleTokenSetAdmin) + + // Emit an event that shows that the contract was initialized + // + emit TokensInitialized(initialSupply: self.totalSupplyByID[0]!) + } +} diff --git a/contracts/FungibleTokens.cdc b/contracts/FungibleTokens.cdc new file mode 100644 index 00000000..155a866e --- /dev/null +++ b/contracts/FungibleTokens.cdc @@ -0,0 +1,248 @@ +/** + +## The Flow Fungible Token standard + +## `FungibleTokens` contract interface + +The FungibleTokens allows a single contract to issue a collection of FungibleTokens + +The interface that all Fungible tokens contracts could conform to. +If a user wants to deploy a new TokenVault contract, their contract would need +to implement the FungibleTokens interface. + +Their contract would have to follow all the rules and naming +that the interface specifies. + +## `TokenVault` resource + +The core resource type that represents an TokenVault in the smart contract. + +## `Collection` Resource + +The resource that stores a user's TokenVault collection. +It includes a few functions to allow the owner to easily +move tokens in and out of the collection. + +## `Provider` and `Receiver` resource interfaces + +These interfaces declare functions with some pre and post conditions +that require the Collection to follow certain naming and behavior standards. + +They are separate because it gives the user the ability to share a reference +to their Collection that only exposes the fields and functions in one or more +of the interfaces. It also gives users the ability to make custom resources +that implement these interfaces to do various things with the tokens. + +By using resources and interfaces, users of TokenVault smart contracts can send +and receive tokens peer-to-peer, without having to interact with a central ledger +smart contract. + +To send an TokenVault to another user, a user would simply withdraw the TokenVault +from their Collection, then call the deposit function on another user's +Collection to complete the transfer. + +*/ + +// The main TokenVault contract interface. Other TokenVault contracts will +// import and implement this interface +// +pub contract interface FungibleTokens { + + // Map of total token supply in existence by type + access(contract) var totalSupplyByID: {UInt64: UFix64} + + // Path to store collection of FungibleTokens minted from implementing contract + pub var CollectionStoragePath: StoragePath + + // Event that emitted when the TokenVault contract is initialized + // + pub event ContractInitialized() + + /// Provider + /// + /// The interface that enforces the requirements for withdrawing + /// tokens from the implementing type. + /// + /// It does not enforce requirements on `balance` here, + /// because it leaves open the possibility of creating custom providers + /// that do not necessarily need their own balance. + /// + pub resource interface Provider { + + /// withdraw subtracts tokens from the owner's TokenVault + /// and returns a TokenVault with the removed tokens. + /// + /// The function's access level is public, but this is not a problem + /// because only the owner storing the resource in their account + /// can initially call this function. + /// + /// The owner may grant other accounts access by creating a private + /// capability that allows specific other users to access + /// the provider resource through a reference. + /// + /// The owner may also grant all accounts access by creating a public + /// capability that allows all users to access the provider + /// resource through a reference. + /// + pub fun withdraw(amount: UFix64): @TokenVault { + post { + // `result` refers to the return value + result.balance == amount: + "Withdrawal amount must be the same as the balance of the withdrawn TokenVault" + } + } + } + + /// Receiver + /// + /// The interface that enforces the requirements for depositing + /// tokens into the implementing type. + /// + /// We do not include a condition that checks the balance because + /// we want to give users the ability to make custom receivers that + /// can do custom things with the tokens, like split them up and + /// send them to different places. + /// + pub resource interface Receiver { + + /// deposit takes a TokenVault and deposits it into the implementing resource type + /// + pub fun deposit(from: @TokenVault) + } + + /// Balance + /// + /// The interface that contains the `balance` field of the TokenVault + /// and enforces that when new TokenVaults are created, the balance and ID + /// are initialized correctly. + /// + pub resource interface Balance { + + /// The total balance of a TokenVault + /// + pub var balance: UFix64 + pub let tokenID: UInt64 + + init(tokenID: UInt64, balance: UFix64) { + post { + self.balance == balance: + "Balance must be initialized to the initial balance" + self.tokenID == tokenID: + "TokenID must be initalized to the supplied tokenID" + } + } + } + + + // Requirement that all conforming TokenVault smart contracts have + // to define a resource called TokenVault that conforms to Provider, Receiver, Balance + pub resource TokenVault: Provider, Receiver, Balance { + + // The declaration of a concrete type in a contract interface means that + // every Fungible Token contract that implements the FungibleToken interface + // must define a concrete `TokenVault` resource that conforms to the `Provider`, `Receiver`, + // and `Balance` interfaces, and declares their required fields and functions + + /// The total balance of the TokenVault + /// + pub var balance: UFix64 + pub let tokenID: UInt64 + + // The conforming type must declare an initializer + // that allows prioviding the initial balance of the TokenVault + // + init(tokenID: UInt64, balance: UFix64) + + /// withdraw subtracts `amount` from the TokenVault's balance + /// and returns a new TokenVault with the subtracted balance + /// + pub fun withdraw(amount: UFix64): @TokenVault { + pre { + self.balance >= amount: + "Amount withdrawn must be less than or equal than the balance of the TokenVault" + } + post { + // use the special function `before` to get the value of the `balance` field + // at the beginning of the function execution + // + self.balance == before(self.balance) - amount: + "New TokenVault balance must be the difference of the previous balance and the withdrawn TokenVault" + } + } + + /// deposit takes a TokenVault and adds its balance to the balance of this TokenVault + /// + pub fun deposit(from: @TokenVault) { + // Assert that the concrete type of the deposited TokenVault is the same + // as the TokenVault that is accepting the deposit + pre { + from.isInstance(self.getType()): + "Cannot deposit an incompatible token type" + } + post { + self.balance == before(self.balance) + before(from.balance): + "New TokenVault balance must be the sum of the previous balance and the deposited TokenVault" + } + } + } + + // Interface that an account would commonly + // publish for their collection + pub resource interface CollectionPublic { + pub fun deposit(token: @TokenVault) + pub fun getIDs(): [UInt64] + } + + pub resource interface CollectionPrivate { + pub fun borrowTokenVault(id: UInt64): &TokenVault + } + + // Requirement for the the concrete resource type + // to be declared in the implementing contract + // + pub resource Collection: CollectionPublic, CollectionPrivate { + + // Dictionary to hold the TokenVaults in the Collection + pub var ownedTokenVaults: @{UInt64: TokenVault} + + // deposit takes a TokenVault and adds it to the collections dictionary + // and adds the ID to the id array + pub fun deposit(token: @TokenVault) + + // getIDs returns an array of the IDs that are in the collection + pub fun getIDs(): [UInt64] + + // Returns a borrowed reference to an TokenVault in the collection + // so that the caller can read data and call methods from it + pub fun borrowTokenVault(id: UInt64): &TokenVault { + pre { + self.ownedTokenVaults[id] != nil: "TokenVault does not exist in the collection!" + } + post { + result.tokenID == id: "Vault with incorrect tokenID returned!" + } + } + } + + // createEmptyCollection creates an empty Collection + // and returns it to the caller so that they can own TokenVaults + pub fun createEmptyCollection(): @Collection { + post { + result.getIDs().length == 0: "The created collection must be empty!" + } + } + + /// createEmptyVault allows any user to create a new TokenVault that has a zero balance (if the tokenID exists) + /// + pub fun createEmptyTokenVault(tokenID: UInt64): @TokenVault { + pre { + self.totalSupplyByID[tokenID] != nil: + "Token ID does not exist in contract" + } + post { + result.balance == 0.0: "The newly created Vault must have zero balance" + result.tokenID == tokenID : "The newly created Vault must have correct tokenID" + } + } + +} \ No newline at end of file