From e51047b2ff08e2cfcb63e1e4a1cdcb33be60b670 Mon Sep 17 00:00:00 2001 From: justjoolz Date: Sat, 5 Feb 2022 16:54:09 +0700 Subject: [PATCH 1/4] added FungibleTokens contract (plural!) --- contracts/FungibleTokens.cdc | 228 +++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 contracts/FungibleTokens.cdc diff --git a/contracts/FungibleTokens.cdc b/contracts/FungibleTokens.cdc new file mode 100644 index 00000000..aed8d90b --- /dev/null +++ b/contracts/FungibleTokens.cdc @@ -0,0 +1,228 @@ +/** + +## 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 + pub var totalSupplyByID: {UInt64: UFix64} + + // 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 Vault + /// and returns a Vault 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 Vault" + } + } + } + + /// 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 Vault and deposits it into the implementing resource type + /// + pub fun deposit(from: @TokenVault) + } + + /// Balance + /// + /// The interface that contains the `balance` field of the Vault + /// and enforces that when new Vaults are created, the balance + /// is initialized correctly. + /// + pub resource interface Balance { + + /// The total balance of a vault + /// + 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 `Vault` resource that conforms to the `Provider`, `Receiver`, + // and `Balance` interfaces, and declares their required fields and functions + + /// The total balance of the vault + /// + pub var balance: UFix64 + pub let tokenID: UInt64 + + // The conforming type must declare an initializer + // that allows prioviding the initial balance of the Vault + // + init(tokenID: UInt64, balance: UFix64) + + /// withdraw subtracts `amount` from the Vault's balance + /// and returns a new Vault 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 Vault" + } + 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 Vault balance must be the difference of the previous balance and the withdrawn Vault" + } + } + + /// deposit takes a Vault and adds its balance to the balance of this Vault + /// + pub fun deposit(from: @TokenVault) { + // Assert that the concrete type of the deposited vault is the same + // as the vault 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 Vault balance must be the sum of the previous balance and the deposited Vault" + } + } + } + + // Interface that an account would commonly + // publish for their collection + pub resource interface CollectionPublic { + pub fun deposit(token: @TokenVault) + pub fun getIDs(): [UInt64] + pub fun borrowTokenVault(id: UInt64): &TokenVault + } + + // Requirement for the the concrete resource type + // to be declared in the implementing contract + // + pub resource Collection: CollectionPublic { + + // 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: "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!" + } + } +} \ No newline at end of file From 87d057768155cdd33062c99874574b15a59f1b5f Mon Sep 17 00:00:00 2001 From: justjoolz Date: Tue, 8 Feb 2022 14:34:28 +0700 Subject: [PATCH 2/4] update interface and add example --- contracts/ExampleTokens.cdc | 255 +++++++++++++++++++++++++++++++++++ contracts/FungibleTokens.cdc | 64 ++++++--- 2 files changed, 297 insertions(+), 22 deletions(-) create mode 100644 contracts/ExampleTokens.cdc diff --git a/contracts/ExampleTokens.cdc b/contracts/ExampleTokens.cdc new file mode 100644 index 00000000..4300173b --- /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 + pub 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 createEmptyVault(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 index aed8d90b..394c4065 100644 --- a/contracts/FungibleTokens.cdc +++ b/contracts/FungibleTokens.cdc @@ -51,6 +51,9 @@ pub contract interface FungibleTokens { // Map of total token supply in existence by type pub 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() @@ -66,8 +69,8 @@ pub contract interface FungibleTokens { /// pub resource interface Provider { - /// withdraw subtracts tokens from the owner's Vault - /// and returns a Vault with the removed tokens. + /// 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 @@ -85,7 +88,7 @@ pub contract interface FungibleTokens { post { // `result` refers to the return value result.balance == amount: - "Withdrawal amount must be the same as the balance of the withdrawn Vault" + "Withdrawal amount must be the same as the balance of the withdrawn TokenVault" } } } @@ -102,20 +105,20 @@ pub contract interface FungibleTokens { /// pub resource interface Receiver { - /// deposit takes a Vault and deposits it into the implementing resource type + /// 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 Vault - /// and enforces that when new Vaults are created, the balance - /// is initialized correctly. + /// 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 vault + /// The total balance of a TokenVault /// pub var balance: UFix64 pub let tokenID: UInt64 @@ -137,48 +140,48 @@ pub contract interface FungibleTokens { // 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 `Vault` resource that conforms to the `Provider`, `Receiver`, + // 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 vault + /// 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 Vault + // that allows prioviding the initial balance of the TokenVault // init(tokenID: UInt64, balance: UFix64) - /// withdraw subtracts `amount` from the Vault's balance - /// and returns a new Vault with the subtracted balance + /// 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 Vault" + "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 Vault balance must be the difference of the previous balance and the withdrawn Vault" + "New TokenVault balance must be the difference of the previous balance and the withdrawn TokenVault" } } - /// deposit takes a Vault and adds its balance to the balance of this Vault + /// 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 vault is the same - // as the vault that is accepting the deposit + // 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 Vault balance must be the sum of the previous balance and the deposited Vault" + "New TokenVault balance must be the sum of the previous balance and the deposited TokenVault" } } } @@ -186,15 +189,18 @@ pub contract interface FungibleTokens { // Interface that an account would commonly // publish for their collection pub resource interface CollectionPublic { - pub fun deposit(token: @TokenVault) + 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 { + pub resource Collection: CollectionPublic, CollectionPrivate { // Dictionary to hold the TokenVaults in the Collection pub var ownedTokenVaults: @{UInt64: TokenVault} @@ -213,7 +219,7 @@ pub contract interface FungibleTokens { self.ownedTokenVaults[id] != nil: "TokenVault does not exist in the collection!" } post { - result.tokenID == id: "Incorrect tokenID returned!" + result.tokenID == id: "Vault with incorrect tokenID returned!" } } } @@ -225,4 +231,18 @@ pub contract interface FungibleTokens { 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 createEmptyVault(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 From eb58864c52d28465019ebd7479d245829843c62a Mon Sep 17 00:00:00 2001 From: justjoolz Date: Wed, 9 Feb 2022 09:20:23 +0700 Subject: [PATCH 3/4] restrict totalSupplyByID field access --- contracts/ExampleTokens.cdc | 2 +- contracts/FungibleTokens.cdc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/ExampleTokens.cdc b/contracts/ExampleTokens.cdc index 4300173b..1239850d 100644 --- a/contracts/ExampleTokens.cdc +++ b/contracts/ExampleTokens.cdc @@ -3,7 +3,7 @@ import FungibleTokens from "./FungibleTokens.cdc" // 0xFungibleTokensADDRESS pub contract ExampleTokens: FungibleTokens { /// Total supply of ExampleTokenSets in existence - pub var totalSupplyByID: {UInt64: UFix64} + access(contract) var totalSupplyByID: {UInt64: UFix64} pub var CollectionStoragePath: StoragePath diff --git a/contracts/FungibleTokens.cdc b/contracts/FungibleTokens.cdc index 394c4065..f9e44475 100644 --- a/contracts/FungibleTokens.cdc +++ b/contracts/FungibleTokens.cdc @@ -49,7 +49,7 @@ Collection to complete the transfer. pub contract interface FungibleTokens { // Map of total token supply in existence by type - pub var totalSupplyByID: {UInt64: UFix64} + access(contract) var totalSupplyByID: {UInt64: UFix64} // Path to store collection of FungibleTokens minted from implementing contract pub var CollectionStoragePath: StoragePath From 39b4b729c987e7cd2bcf1424fa0ea2e3782eb51a Mon Sep 17 00:00:00 2001 From: justjoolz Date: Wed, 9 Feb 2022 09:41:07 +0700 Subject: [PATCH 4/4] change fun name to reflect required tokenID --- contracts/ExampleTokens.cdc | 2 +- contracts/FungibleTokens.cdc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/ExampleTokens.cdc b/contracts/ExampleTokens.cdc index 1239850d..dd4433da 100644 --- a/contracts/ExampleTokens.cdc +++ b/contracts/ExampleTokens.cdc @@ -113,7 +113,7 @@ pub contract ExampleTokens: FungibleTokens { /// 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(tokenID: UInt64): @TokenVault { + pub fun createEmptyTokenVault(tokenID: UInt64): @TokenVault { return <-create TokenVault(tokenID: tokenID, balance: 0.0) } diff --git a/contracts/FungibleTokens.cdc b/contracts/FungibleTokens.cdc index f9e44475..155a866e 100644 --- a/contracts/FungibleTokens.cdc +++ b/contracts/FungibleTokens.cdc @@ -234,7 +234,7 @@ pub contract interface FungibleTokens { /// createEmptyVault allows any user to create a new TokenVault that has a zero balance (if the tokenID exists) /// - pub fun createEmptyVault(tokenID: UInt64): @TokenVault { + pub fun createEmptyTokenVault(tokenID: UInt64): @TokenVault { pre { self.totalSupplyByID[tokenID] != nil: "Token ID does not exist in contract"