diff --git a/contracts/ExampleToken-v2.cdc b/contracts/ExampleToken-v2.cdc new file mode 100644 index 00000000..f85ba705 --- /dev/null +++ b/contracts/ExampleToken-v2.cdc @@ -0,0 +1,268 @@ +import FungibleToken from "./FungibleToken-v2.cdc" +import MetadataViews from "./utility/MetadataViews.cdc" +import FungibleTokenMetadataViews from "./FungibleTokenMetadataViews.cdc" + +pub contract ExampleToken: FungibleToken { + + /// Total supply of ExampleTokens in existence + access(contract) var totalSupply: {Type: UFix64} + + /// Admin Path + pub let AdminStoragePath: StoragePath + + /// EVENTS + /// TokensWithdrawn + /// + /// The event that is emitted when tokens are withdrawn from a Vault + pub event TokensWithdrawn(amount: UFix64, from: Address?, type: Type) + + /// TokensDeposited + /// + /// The event that is emitted when tokens are deposited to a Vault + pub event TokensDeposited(amount: UFix64, to: Address?, type: Type) + + /// TokensTransferred + /// + /// The event that is emitted when tokens are transferred from one account to another + pub event TokensTransferred(amount: UFix64, from: Address?, to: Address?, type: Type) + + /// TokensMinted + /// + /// The event that is emitted when new tokens are minted + pub event TokensMinted(amount: UFix64, type: Type) + + /// TokensBurned + /// + /// The event that is emitted when tokens are destroyed + pub event TokensBurned(amount: UFix64, type: Type) + + /// Function to return the types that the contract implements + pub fun getVaultTypes(): {Type: FungibleTokenMetadataViews.FTView} { + let typeDictionary: {Type: FungibleTokenMetadataViews.FTView} = {} + + let vault <- create Vault(balance: 0.0) + + typeDictionary[vault.getType()] = vault.resolveView(Type()) as! FungibleTokenMetadataViews.FTView + + destroy vault + + return typeDictionary + } + + /// 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.Vault, FungibleToken.Provider, FungibleToken.Transferable, FungibleToken.Receiver, FungibleToken.Balance { + + /// The total balance of this vault + pub var balance: UFix64 + + access(self) var storagePath: StoragePath + access(self) var publicPath: PublicPath + + pub fun getViews(): [Type] { + return [Type(), + Type(), + Type()] + } + + 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: "Example Fungible Token", + symbol: "EFT", + description: "This fungible token is used as an example to help you develop your next FT #onFlow.", + externalURL: MetadataViews.ExternalURL("https://example-ft.onflow.org"), + logos: medias, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + case Type(): + let vaultRef = self.account.borrow<&ExampleToken.Vault>(from: self.storagePath) + ?? panic("Could not borrow a reference to the stored vault") + return FungibleTokenMetadataViews.FTVaultData( + storagePath: self.storagePath, + receiverPath: self.publicPath, + metadataPath: self.publicPath, + providerPath: /private/exampleTokenVault, + receiverLinkedType: Type<&ExampleToken.Vault{FungibleToken.Receiver}>(), + metadataLinkedType: Type<&ExampleToken.Vault{FungibleToken.Balance, MetadataViews.Resolver}>(), + providerLinkedType: Type<&ExampleToken.Vault{FungibleToken.Provider}>(), + createEmptyVaultFunction: (fun (): @ExampleToken.Vault{FungibleToken.Vault} { + return <-vaultRef.createEmptyVault + }) + ) + } + return nil + } + + /// getSupportedVaultTypes optionally returns a list of vault types that this receiver accepts + pub fun getSupportedVaultTypes(): {Type: Bool} { + let supportedTypes: {Type: Bool} = {} + supportedTypes[self.getType()] = true + return supportedTypes + } + + // initialize the balance at resource creation time + init(balance: UFix64) { + self.balance = balance + let identifier = "exampleTokenVault" + self.storagePath = StoragePath(identifier: identifier)! + self.publicPath = PublicPath(identifier: identifier)! + } + + /// Get the balance of the vault + pub fun getBalance(): UFix64 { + return self.balance + } + + /// 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 tokens that are being transferred. It returns the newly + /// created Vault to the context that called so it can be deposited + /// elsewhere. + /// + pub fun withdraw(amount: UFix64): @ExampleToken.Vault{FungibleToken.Vault} { + self.balance = self.balance - amount + emit TokensWithdrawn(amount: amount, from: self.owner?.address, type: self.getType()) + 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: @AnyResource{FungibleToken.Vault}) { + let vault <- from as! @ExampleToken.Vault + self.balance = self.balance + vault.balance + emit TokensDeposited(amount: vault.balance, to: self.owner?.address, type: self.getType()) + vault.balance = 0.0 + destroy vault + } + + pub fun transfer(amount: UFix64, recipient: Capability<&{FungibleToken.Receiver}>) { + let transferVault <- self.withdraw(amount: amount) + + // Get a reference to the recipient's Receiver + let receiverRef = recipient.borrow() + ?? panic("Could not borrow receiver reference to the recipient's Vault") + + // Deposit the withdrawn tokens in the recipient's receiver + receiverRef.deposit(from: <-transferVault) + + emit TokensTransferred(amount: amount, from: self.owner?.address, to: receiverRef.owner?.address, type: self.getType()) + } + + /// 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(): @ExampleToken.Vault{FungibleToken.Vault} { + return <-create Vault(balance: 0.0) + } + + destroy() { + if self.balance > 0.0 { + ExampleToken.totalSupply[self.getType()] = ExampleToken.totalSupply[self.getType()]! - self.balance + } + } + } + + /// Minter + /// + /// Resource object that token admin accounts can hold to mint new tokens. + /// + pub resource Minter { + /// mintTokens + /// + /// Function that mints new tokens, adds them to the total supply, + /// and returns them to the calling context. + /// + pub fun mintTokens(amount: UFix64): @ExampleToken.Vault { + ExampleToken.totalSupply[self.getType()] = ExampleToken.totalSupply[self.getType()]! + amount + emit TokensMinted(amount: amount, type: self.getType()) + return <-create Vault(balance: amount) + } + } + + /// 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(vaultType: Type): @{FungibleToken.Vault}? { + switch vaultType { + case Type<@ExampleToken.Vault>(): + return <- create Vault(balance: 0.0) + default: + return nil + } + } + + init() { + self.totalSupply = {} + self.totalSupply[Type<@ExampleToken.Vault>()] = 1000.0 + + self.AdminStoragePath = /storage/exampleTokenAdmin + + // Create the Vault with the total supply of tokens and save it in storage + // + let vault <- create Vault(balance: self.totalSupply[Type<@ExampleToken.Vault>()]!) + let ftView = vault.resolveView(Type()) as! FungibleTokenMetadataViews.FTVaultData + + let storagePath = ftView.storagePath + let receiverBalancePath = ftView.receiverPath + + self.account.save(<-vault, to: storagePath) + + // Create a public capability to the stored Vault that exposes + // the `deposit` method and getAcceptedTypes method through the `Receiver` interface + // and the `getBalance()` method through the `Balance` interface + // + self.account.link<&{FungibleToken.Receiver, FungibleToken.Balance}>( + receiverBalancePath, + target: storagePath + ) + + let admin <- create Minter() + self.account.save(<-admin, to: self.AdminStoragePath) + } +} + \ No newline at end of file diff --git a/contracts/FungibleToken-v2.cdc b/contracts/FungibleToken-v2.cdc new file mode 100644 index 00000000..ea6b5ab9 --- /dev/null +++ b/contracts/FungibleToken-v2.cdc @@ -0,0 +1,234 @@ +/** + +# The Flow Fungible Token standard + +## `FungibleToken` contract + +The Fungible Token standard is no longer an interface +that all fungible token contracts would have to conform to. + +If a users wants to deploy a new token contract, their contract +does not need to implement the FungibleToken interface, but their tokens +do need to implement the interfaces defined in this contract. + +## `Vault` resource interface + +Each fungible token resource type needs to implement the `Vault` resource interface. + +## `Provider`, `Receiver`, and `Balance` resource interfaces + +These interfaces declare pre-conditions and post-conditions that restrict +the execution of the functions in the Vault. + +They are separate because it gives the user the ability to share +a reference to their Vault that only exposes the fields 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. +For example, a faucet can be implemented by conforming +to the Provider interface. + +*/ + +import FungibleTokenMetadataViews from "./FungibleTokenMetadataViews.cdc" + +/// FungibleToken +/// +/// Fungible Token implementations are no longer required to implement the fungible token +/// interface. We still have it as an interface here because there are some useful +/// utility methods that many projects will still want to have on their contracts, +/// but they are by no means required. all that is required is that the token +/// implements the `Vault` interface +pub contract interface FungibleToken { + + /// TokensWithdrawn + /// + /// The event that is emitted when tokens are withdrawn from a Vault + pub event TokensWithdrawn(amount: UFix64, from: Address?, type: Type, ftView: FungibleTokenMetadataViews.FTView) + + /// TokensDeposited + /// + /// The event that is emitted when tokens are deposited to a Vault + pub event TokensDeposited(amount: UFix64, to: Address?, type: Type, ftView: FungibleTokenMetadataViews.FTView) + + /// TokensTransferred + /// + /// The event that is emitted when tokens are transferred from one account to another + pub event TokensTransferred(amount: UFix64, from: Address?, to: Address?, type: Type, ftView: FungibleTokenMetadataViews.FTView) + + /// TokensMinted + /// + /// The event that is emitted when new tokens are minted + pub event TokensMinted(amount: UFix64, type: Type, ftView: FungibleTokenMetadataViews.FTView) + + /// Contains the total supply of the fungible tokens defined in this contract + access(contract) var totalSupply: {Type: UFix64} + + /// Function to return the types that the contract implements + pub fun getVaultTypes(): {Type: FungibleTokenMetadataViews.FTView} { + post { + result.length > 0: "Must indicate what fungible token types this contract defines" + } + } + + /// 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): @AnyResource{Vault} { + post { + // `result` refers to the return value + result.getBalance() == amount: + "Withdrawal amount must be the same as the balance of the withdrawn Vault" + } + } + } + + pub resource interface Transferable { + /// Function for a direct transfer instead of having to do a deposit and withdrawal + /// + pub fun transfer(amount: UFix64, recipient: Capability<&{FungibleToken.Receiver}>) + } + + /// 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: @AnyResource{Vault}) + + /// getSupportedVaultTypes optionally returns a list of vault types that this receiver accepts + pub fun getSupportedVaultTypes(): {Type: Bool} + } + + /// Balance + /// + /// This interface is now a general purpose metadata interface because + /// a public interface is needed to get metadata, but adding a whole new interface + /// for every account to upgrade to is probably too much of a breaking change + pub resource interface Balance { + + /// Method to get the balance + /// The balance could be a derived field, + /// so there is no need to require an explicit field + pub fun getBalance(): UFix64 + + pub fun getSupportedVaultTypes(): {Type: Bool} + + /// MetadataViews Methods + /// + pub fun getViews(): [Type] { + return [] + } + + pub fun resolveView(_ view: Type): AnyStruct? { + return nil + } + } + + /// Vault + /// + /// Ideally, this interface would also conform to Receiver, Balance, Transferable, and Provider, + /// but that is not supported yet + /// + pub resource interface Vault { //: Receiver, Balance, Transferable, Provider { + + /// Get the balance of the vault + pub fun getBalance(): UFix64 + + /// getSupportedVaultTypes optionally returns a list of vault types that this receiver accepts + pub fun getSupportedVaultTypes(): {Type: Bool} + + pub fun getViews(): [Type] + pub fun resolveView(_ view: Type): AnyStruct? + + /// withdraw subtracts `amount` from the Vault's balance + /// and returns a new Vault with the subtracted balance + /// + pub fun withdraw(amount: UFix64): @AnyResource{Vault} { + pre { + self.getBalance() >= 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.getBalance() == before(self.getBalance()) - amount: + "New Vault balance must be the difference of the previous balance and the withdrawn Vault balance" + } + } + + /// deposit takes a Vault and adds its balance to the balance of this Vault + /// + pub fun deposit(from: @AnyResource{FungibleToken.Vault}) { + // 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.getBalance() == before(self.getBalance()) + before(from.getBalance()): + "New Vault balance must be the sum of the previous balance and the deposited Vault" + } + } + + /// Function for a direct transfer instead of having to do a deposit and withdrawal + /// + pub fun transfer(amount: UFix64, recipient: Capability<&{FungibleToken.Receiver}>) { + post { + self.getBalance() == before(self.getBalance()) - amount: + "New Vault balance from the sender must be the difference of the previous balance and the withdrawn Vault balance" + } + } + + /// createEmptyVault allows any user to create a new Vault that has a zero balance + /// + pub fun createEmptyVault(): @AnyResource{Vault} { + post { + result.getBalance() == 0.0: "The newly created Vault must have zero balance" + } + } + } + + /// createEmptyVault allows any user to create a new Vault that has a zero balance + /// + pub fun createEmptyVault(vaultType: Type): @AnyResource{Vault}? { + post { + result.getBalance() == 0.0: "The newly created Vault must have zero balance" + } + } +} \ No newline at end of file diff --git a/lib/go/contracts/contracts.go b/lib/go/contracts/contracts.go index 0e0be51e..d186d360 100644 --- a/lib/go/contracts/contracts.go +++ b/lib/go/contracts/contracts.go @@ -13,6 +13,7 @@ import ( var ( placeholderFungibleToken = regexp.MustCompile(`"[^"\s].*/FungibleToken.cdc"`) + placeholderFungibleTokenV2 = regexp.MustCompile(`"[^"\s].*/FungibleToken-v2.cdc"`) placeholderExampleToken = regexp.MustCompile(`"[^"\s].*/ExampleToken.cdc"`) placeholderMetadataViews = regexp.MustCompile(`"[^"\s].*/MetadataViews.cdc"`) placeholderFTMetadataViews = regexp.MustCompile(`"[^"\s].*/FungibleTokenMetadataViews.cdc"`) @@ -20,11 +21,12 @@ var ( const ( filenameFungibleToken = "FungibleToken.cdc" + filenameFungibleTokenV2 = "FungibleToken-v2.cdc" filenameExampleToken = "ExampleToken.cdc" + filenameExampleTokenV2 = "ExampleToken-v2.cdc" filenameTokenForwarding = "utility/TokenForwarding.cdc" filenamePrivateForwarder = "utility/PrivateReceiverForwarder.cdc" filenameFTMetadataViews = "FungibleTokenMetadataViews.cdc" - filenameFTSwitchboard = "FungibleTokenSwitchboard.cdc" ) // FungibleToken returns the FungibleToken contract interface. @@ -34,6 +36,11 @@ func FungibleToken() []byte { return []byte(code) } +// FungibleTokenV2 returns the FungibleToken-v2 contract. +func FungibleTokenV2() []byte { + return assets.MustAsset(filenameFungibleTokenV2) +} + // FungibleToken returns the FungibleToken contract interface. func FungibleTokenMetadataViews(fungibleTokenAddr, metadataViewsAddr string) []byte { code := assets.MustAssetString(filenameFTMetadataViews) @@ -66,6 +73,17 @@ func ExampleToken(fungibleTokenAddr, metadataViewsAddr, ftMetadataViewsAddr stri return []byte(code) } +// ExampleTokenV2 returns the second version of the ExampleToken contract. +// +// The returned contract will import the FungibleToken interface from the specified address. +func ExampleTokenV2(fungibleTokenAddr string) []byte { + code := assets.MustAssetString(filenameExampleTokenV2) + + code = placeholderFungibleTokenV2.ReplaceAllString(code, "0x"+fungibleTokenAddr) + + return []byte(code) +} + // CustomToken returns the ExampleToken contract with a custom name. // // The returned contract will import the FungibleToken interface from the specified address. diff --git a/lib/go/test/token_test_helpers.go b/lib/go/test/token_test_helpers.go index 084984a2..86e066eb 100644 --- a/lib/go/test/token_test_helpers.go +++ b/lib/go/test/token_test_helpers.go @@ -136,3 +136,67 @@ func DeployTokenContracts( return fungibleAddr, tokenAddr, forwardingAddr, metadataViewsAddr } + +// Deploys the FungibleToken-V2, ExampleToken, and TokenForwarding contracts +// to different accounts and returns their addresses +func DeployV2TokenContracts( + b *emulator.Blockchain, + t *testing.T, + key []*flow.AccountKey, +) ( + fungibleAddr flow.Address, + //tokenAddr flow.Address, + //forwardingAddr flow.Address, +) { + var err error + + // Deploy the FungibleToken contract + fungibleTokenCode := contracts.FungibleTokenV2() + fungibleAddr, err = b.CreateAccount( + nil, + []sdktemplates.Contract{ + { + Name: "FungibleToken", + Source: string(fungibleTokenCode), + }, + }, + ) + assert.NoError(t, err) + + _, err = b.CommitBlock() + assert.NoError(t, err) + + // Deploy the ExampleToken contract + exampleTokenCode := contracts.ExampleTokenV2(fungibleAddr.String()) + _, err = b.CreateAccount( + key, + []sdktemplates.Contract{ + { + Name: "ExampleToken", + Source: string(exampleTokenCode), + }, + }, + ) + assert.NoError(t, err) + + _, err = b.CommitBlock() + assert.NoError(t, err) + + // // Deploy the TokenForwarding contract + // forwardingCode := contracts.TokenForwarding(fungibleAddr.String()) + // forwardingAddr, err = b.CreateAccount( + // key, + // []sdktemplates.Contract{ + // { + // Name: "TokenForwarding", + // Source: string(forwardingCode), + // }, + // }, + // ) + // assert.NoError(t, err) + + // _, err = b.CommitBlock() + // assert.NoError(t, err) + + return fungibleAddr //, nil, nil +} diff --git a/lib/go/test/token_v2_test.go b/lib/go/test/token_v2_test.go new file mode 100644 index 00000000..73d38c64 --- /dev/null +++ b/lib/go/test/token_v2_test.go @@ -0,0 +1,29 @@ +package test + +import ( + "testing" + + // sdktemplates "github.com/onflow/flow-go-sdk/templates" + // "github.com/stretchr/testify/assert" + // "github.com/stretchr/testify/require" + + // "github.com/onflow/cadence" + // jsoncdc "github.com/onflow/cadence/encoding/json" + "github.com/onflow/flow-go-sdk" + // "github.com/onflow/flow-go-sdk/crypto" + // "github.com/onflow/flow-ft/lib/go/contracts" + // "github.com/onflow/flow-ft/lib/go/templates" +) + +func TestV2TokenDeployment(t *testing.T) { + b, accountKeys := newTestSetup(t) + + exampleTokenAccountKey, _ := accountKeys.NewWithSigner() + _ = DeployV2TokenContracts(b, t, []*flow.AccountKey{exampleTokenAccountKey}) + + // t.Run("Should have initialized Supply field correctly", func(t *testing.T) { + // script := templates.GenerateInspectSupplyScript(fungibleAddr, exampleTokenAddr, "ExampleToken") + // supply := executeScriptAndCheck(t, b, script, nil) + // assert.Equal(t, CadenceUFix64("1000.0"), supply) + // }) +}