Streamlined Token Standards Proposal

NOTE: The designs of the new standards have evolved significantly since this was originally posted, so many of the discussion points and code samples are out-of-date. Please refer to the FLIPs and PRs for the latest versions of the v2 standards.

Streamlined Token Standards

Stable Cadence refers to a major milestone in the evolution of the Cadence programming language where there will no longer be any more breaking changes to Cadence. There are a number of changes that the Cadence community would like to make to Cadence before this milestone. Several proposals can be found in this forum post. We hope to see the Cadence programming language (and apps that use it) last for many many years, so it is important that we set everything up to be as safe and easy to use as possible in the long term.

One of the biggest changes we would like to see made is a refactoring and streamlining of the fungible and non-fungible token standards with the goal of making them cleaner and more powerful.

The current fungible and non-fungible token standards for Flow (henceforth referred to as “the token standards”) were designed in mid 2019, at a time when Cadence itself was still being designed. The current standards, though functional, leave much to be desired. (we believe can be improved upon) We have learned a lot in the three years since and want to make a fundamental improvement to them, as well as some other smaller changes.

First, we’ll discuss the shortcomings of the current standards, then we’ll address the core update proposal, and finally mention other smaller proposals at the end.

These changes would be breaking for all fungible tokens and NFTs on Flow, but we will provide a clear and easy upgrade and migration path for all projects to follow, as well as provide assistance in the migration process. To make the process easier, our plan is to provide a window for projects to upgrade their contracts and downstream dependencies before the old version of Cadence is phased out.

We’ll also propose a feature to allow developers to pre- upload revised contracts addressing the changes. These contracts will become effective in the next spork which updates the Cadence version.

Current Token Standards

Before reading about the proposal, please make sure you are familiar with the current token standards for Flow:

The current token standards use contract interfaces. They are designed in a way which requires each concrete contract to provide exactly one Vault or NFT type. This means that any project that needs multiple tokens must deploy multiple contracts. In the case of very simple tokens, this is a lot of complexity for very little value. Additionally, projects cannot give a custom name for their NFT because of these type requirements.

// Reduced Example.
 
// Contracts that import that standard must define one vault that
// implements FungibleToken.Vault and can’t add any more
pub contract interface FungibleToken {
   // Can only define one Vault that is compatible with the standard
   pub resource Vault: Provider, Receiver, Balance {
       pub var balance: UFix64
       pub fun withdraw(amount: UFix64): @Vault { /* … */ }
       pub fun deposit(from: @Vault) { /* … */ }
   }
}

Additionally, implementing the NFT standard is more complex than it needs to be, since each NFT implementor also needs to implement their own Collection resource. (The Collection implementation of the Example NFT contract is 5x as long as the NFT implementation itself!)

// Reduced example of NonFungibleToken
pub contract interface NonFungibleToken {
   // Projects can only define one token type that conforms to the standard
   pub resource NFT {
       pub let id: UInt64
   }
   // Projects must also implement a custom collection type,
   // for the specific new NFT.
   // This shouldn’t be necessary,
   // a collection should be able to be heterogeneous
   pub resource Collection: Provider, Receiver, CollectionPublic {
       pub var ownedNFTs: @{UInt64: NFT}
       pub fun withdraw(withdrawID: UInt64): @NFT
       pub fun deposit(token: @NFT)
       pub fun getIDs(): [UInt64]
       pub fun borrowNFT(id: UInt64): &NFT
   }

The motivation for using contract interfaces in the first place was in the hope that the Receiver and Provider types in the concrete token interfaces would be statically checkable, e.g. that FlowToken.Provider would have FlowToken.Vault as its return type, instead of FungibleToken.Vault. Thanks to the vagaries of “variance” in programming language type systems this isn’t actually possible.

So, the value of using contract interfaces and especially type requirements was never realized, and now we are just stuck with the cost.

Nested Type Requirements in Cadence are a fairly advanced concept. Just like an interface may require a conforming type to provide a certain field or function, it may also require the conforming type to provide a nested type. This is uncommon in other programming languages, so newcomers to Cadence need to learn and understand this feature to understand how a simple Fungible Token can be implemented.

Fungible token contracts need to provide a way to create an empty vault. As resources may only be created/constructed inside the contract they are defined in, the fungible token standard requires fungible token contracts to provide/implement a function createEmptyVault. This makes it hard to reason about the code of a fungible token, as code related to the creation of a vault is partially implemented in the vault resource (the initializer), and partially implemented in the contract (the createEmptyVault function). This is also the reason why a vault type currently needs to be defined inside of a contract, and cannot be defined alongside it.

Potential Solutions

There are several potential ways the current standards could be improved. We have come up with a proposal that we believe addresses these issues. However, we are not fully committed to the specific proposed changes and would also like to strongly encourage improvements to this suggestion, as well as alternative solutions.

This proposal tries to address some of the issues described by encapsulating most of the functionality of tokens in the resources and/or resource interfaces themselves, instead of as nested type requirements in the contract. This should simplify adoption of standards, making it possible to define multiple vaults and NFTs per contract, with specific names instead of generic “NFT.”

We also propose defining event types within the vaults themselves. This also requires an update to Cadence to allow type definitions within resources, which will be a separate FLIP.

By introducing static functions as a language feature (like in the suggested standard below), the function to create an empty vault could be added to the Vault resource itself, instead of being defined in the contract. This allows keeping code related to vault creation in one place. The addition of static functions to Cadence will also be in its own FLIP.

The proposal also includes some other changes that have been proposed in the past, such as:

  • introducing a transfer function
  • using default implementations for functions (specifically related to NFT metadata)
  • not requiring an NFT ID field to be a specific value or type
  • using a reusable NFT collection instead of requiring each project to implement a collection for a specific NFT.

New Token Standards

Below are examples of what the token standards could look like if we implemented the changes proposed above.

Proposed Fungible Token Interfaces

The current contract interface would be replaced by something similar to the following:

// A shared contract, deployed in a well-known location.
pub contract FungibleToken {
    pub resource interface Provider {
        pub fun withdraw(amount: UFix64): @{Vault} {
            post {
                result.balance == amount
                 }
        }

        pub fun transfer(amount: UFix64, recipient: &AnyResource{Receiver})
    }

    pub resource interface Receiver {
        pub fun deposit(from: @{Vault})
    }

    pub resource interface Balance {
        pub fun getBalance(): UFix64
    }

    pub resource interface Vault: Provider, Receiver {

        // The proposal only requires a function to get the balance
        // of a vault because some projects may want to compute a balance
        // instead of storing it as a single field
        pub fun getBalance(): UFix64

        pub fun withdraw(amount: UFix64): @{Vault} {
            pre {
                self.getBalance() >= amount
                 }
            post {
                self.getBalance() == before(self.getBalance()) - amount
                 }
        }

        pub fun deposit(from: @{Vault}) {
            post {
                self.getBalance() == before(self.getBalance()) + before(from.getBalance())
                 }
        }

        pub fun transfer(amount: UFix64, recipient: &AnyResource{Receiver}) {}
    }
}

New Fungible Token Example Implementation

// A specific contract, deployed into a user account
import FungibleToken from 0x02
import StandardMetadata from 0x04

pub contract TokenExample {
    pub resource ExampleVault: FungibleToken.Vault {

		 // events could be defined in the contract, or potentially
        // in the resource itself. This is an implementation detail
        pub event TokensWithdrawn(amount: UFix64, from: Address?)
        pub event TokensDeposited(amount: UFix64, to: Address?)

        access(self) var balance: UFix64

        init(balance: UFix64) {
            self.balance = balance
        }

		// Code for creating new vaults can now be in the vault
       // instead of separate
        pub static fun createEmpty(): @ExampleVault {}
            return <-create ExampleVault(balance: 0.0)
        }

        pub fun withdraw(amount: UFix64): @ExampleVault {
            self.balance = self.balance - amount
            return <-create Vault(balance: amount)
        }

        pub fun deposit(from: @ExampleVault) {
            self.balance = self.balance + from.balance
            from.balance = 0.0
            destroy from
        }

        pub fun transfer(amount: UFix64, recipient: &AnyResource{Receiver}) {

            let tokens <- self.withdraw(amount: amount)

            recipient.deposit(from: <-tokens)

        }
        
        pub fun getBalance(): UFix64 {
            return self.balance        
        }
        
    }

Proposed NonFungibleToken Interfaces

// A shared contract, deployed in a well-known location.
pub contract NonFungibleToken {

   // We will use a generic NFT collection, so we can use standard paths
   // in the standard contract
   pub let collectionStoragePath: StoragePath
   pub let collectionPublicPath: PublicPath

   pub event Withdraw(type: Type, id: UFix64, from: Address?)
   pub event Deposit(type: Type, id: UFix64, to: Address?)
   pub event Transfer(type: Type, id: UFix64, from: Address?, to: Address)

    pub resource interface NFT {
        // We also don’t want to restrict how an NFT defines its ID
        // They can store it, compute it, or use the UUID
        pub fun getID(): UInt64

        // Two functions for the NFT Metadata Standard
        pub fun getViews() : [Type]
        pub fun resolveView(_ view:Type): AnyStruct?
    }

    // This is not the final proposal for provider. Ideally, we could have a provider
    // interface that is only valid for a specific NFT type of array of IDs so that the owner
    // can protect NFTs from being accessed unintentionally
    pub resource interface Provider {
        pub fun withdraw(type: Type, withdrawID: UInt64): @{NFT}? {
            post {
                result.getID() == withdrawID
            }
        }

        pub fun transfer(type: Type, withdrawID: UInt64, recipient: &AnyResource{Receiver})
        
    }

    pub resource interface Receiver {
        pub fun deposit(token: @{NFT})
    }

    pub resource interface CollectionPublic: Receiver {
        // A user will likely have a way to restrict which NFTs
        // they want to receive so they can’t get spammed
        pub fun deposit(token: @AnyResource{NFT})
        pub fun getIDs(): {Type: [UInt64]}
        pub fun borrowNFT(id: UInt64): &{NFT}?
    }
    
    
    // A full implementation of NFT collection that
    // *is not* specific to one NFT type
    // Users could just use an instance of this to store all their 
    // NFTs instead of having to use a specific one for each project
    //
    pub resource NFTCollection: CollectionPublic, Receiver, Provider {
        access(self) let ownedNFTs: {Type: {UInt64: @{NFT}}

        pub fun deposit(token: @AnyResource{NFT}) {
			pre {
                 // Potentially allow users to specify which NFT types
                 // they are comfortable receiving so they don’t
                 // get spammed with NFTs 
             }

        }

        pub fun withdraw(type: Type, withdrawID: UInt64): @{NFT}?  {

        }

        pub fun transfer(type: Type, withdrawID: UInt64, recipient: &AnyResource{Receiver}) {

        }

        // Also could potentially include batch withdraw, batch deposit,
        // and batch transfer

        pub fun getIDs(): {Type: [UInt64] {

             }

        // Could also include a method that includes a subset of the IDs
        pub fun getIDsPaginated(subset: {???}): {Type: [UInt64]} {}

        pub fun borrowNFT(id: UInt64): &{NFT}? {
 
        }

    }
}

New Non-Fungible Token Example Implementation

// A specific contract, deployed into a user account
import NonFungibleToken from 0x03
import StandardMetadata from 0x04

pub contract ExampleTokenImplementation

    pub resource ExampleNFT: NonFungibleToken.NFT {
        // Could also use uuid as the unique identifier
        access(self) let name: String
        
        pub fun getID(): UInt64 {
            // this could also use a id field if needed
            return self.uuid
        }

		// From the NFT Metadata Standard
        pub fun getViews(): [Type] {
           return [StandardMetadata.SimpleName]
        }

		// From the NFT Metadata Standard
        pub fun resolveView(_ view:Type): AnyStruct? {
            if view == StandardMetadata.SimpleName {
                return StandardMetadata.SimpleName(name: self.name)
            }
            return nil
        }

        init(initID: UInt64) {
            self.id = initID
            self.name = "Token name"
        }
    }
}

Some things worth noting from the examples:

  • The project doesn’t have to worry about adding boilerplate code for implementing an nft collection resource. Since the standard does not use a contract interface any more and makes NFTs more generic, users can use a single collection for all their NFTs.
  • We no longer use a contract interface for FungibleToken or NonFungibleToken, so some standard pieces like the totalSupply field and the standard events are not included in the contract. The totalSupply field could potentially be handled by a simple contract interface that only specifies the totalSupply field. Replacing the standard events is more difficult because we’ve decided in Cadence that we’d like to avoid type requirements in interfaces. To address this, we propose that projects define their own event types in their contract. There will still be a standard for event names and parameters, but it will not be enforced by the contract interface anymore. This is definitely a topic that is still up for debate.
  • Another benefit of using resource interfaces instead of contract interfaces is that a project can define any number of fungible tokens and/or non-fungible tokens in the same contract, allowing a lot more flexibility and efficiency for developers.
  • We have included standard storage and public paths for the nft collection. Since each user will use the same collection, there is no need for projects to have their own paths.
  • We have also included a transfer method to make transfers a bit more straightforward, a feature requested by many community members.
  • There is an open issue for improvements to the existing NFT standard that was discussed in 2021. Many of those improvements are addressed by this standard, but not all. Some of them are no longer relevant because of the nature of the refactoring of the standard, while some of them require additional changes to Cadence not in the scope of these upgrades.
    Some of these improvements that are out of scope are:
    • Enumerating resources in an account
    • Using generics
    • Requiring that a certain event is emitted in a specific function.
    • NFT nonce

We recognize these are not trivial changes, but we are confident that by collaborating with the community, we can devise an upgrade path for everyone that minimizes the difficulty associated with upgrading. If this initial proposal gets good feedback, we’ll move forward with defining a detailed upgrade path and migration plan. This will also need to be approved by the community before committing to the upgrade. The Flow team is sure that the effort required by everyone will be worth it in the long run and produce clearer, safer, and more composable token standards.

2 Likes

This should return an optional AnyStruct if you want it to be compatible with the current method.

I have read through this and will probably comment more later, but in general I like these suggestions.

There is however one part i do not like. And that is identification. I belive all nfts should use uuid as the id that is used to store it in the collection. If not there will be conflicts and problems.

I also belive a generic collection should have some more helper methods to lookup all items of a give type and subcollection. Like «all starly cards made by afterfuture».

2 Likes

Thank you for the comments! I think your last two are mostly implementation details that can focus on later when we actually make the FLIP and real code proposals. This is just meant to be a pseudo-code example.

I agree that also NFTs should use the uuid, but this is one difficulty that we will face with upgrades and with using interfaces. A lot of projects already use non-uuids for their IDs and it isn’t clear yet if we’ll be able to scrap that completely or just allow both versions to live in harmony. With the generic NFT collection, the stored NFTs will likely still be separated by type, so it won’t be an issue there though.

1 Like

I think this proposal is very interesting. It seems useful to be able to define multiple NFT resources within a single contract, for instance. That’s something our team recently had to work around.

I’m concerned by the comment about NFT projects not implementing individual collections though (although from a very high-level it seems like a neat goal, but practically, I think there are challenges). Is your suggestion that after the Secure Cadence upgrades all NFTs will be held in a single NFT collection for all users? Would this be a collection provided by Dapper, would it come with an initialized account? Or is the idea to allow NFT projects to define their own Collections if they choose, but also provide a network-wide default Collection?

I think projects, ours included, can have interesting uses for defining a collection in a custom way (i.e. hooking into the deposit, withdraw functions and storing data about deposits/withdraws on an ownership ledger, other things in terms of unique utility). Forcing all projects to use a standard collection seems as though it would be highly limiting, and also break existing projects in a way that would be very difficult to fix.

2 Likes

Thanks a bunch for sharing these in public. Happy to revisit this topic of standards.

In regards to the NonFungibleToken standard, I’m concerned with the newest updates to support multiple NFTs per contract. To me, it seems like minimal work was taken off of users creating NFTs and put way way way way more effort onto people interacting with the NFTs.

My main concern is wondering why we want to support multiple NFTs per contract. I don’t see any issues with the current multiple-contract approach as is. What is hindering developers by doing that? The only downside I can see is requiring a collection for each NFT type, but I don’t really think this is an issue. Encouraging a collection for each NFT type seems intuitive and simple to me, even if it requires a few extra lines of code. Trying to shove them into one generalized collection seems extremely dangerous. If someone rugs a project and links your generalized collection to the public, it’s essentially like leaking your private key. I think this alone should be reason enough not to implement this feature for the average user.

Also, this idea sort of breaks the idea of a standard, since now some projects will store their NFTs in a generalized collection, and some will still implement their own collections because they want to add certain features. So now discoverability will require checking in multiple places, thus breaking an official standard.

Lastly, these changes seem to be very major, and introduce a lot of difficult fixes for current projects. I definitely understand the need for breaking changes, but these seem like overkill and mostly limiting

4 Likes

Great work @flowjosh, I really liked the proposal. Not usual for me, but I couldn’t find anything to criticize :wink:

Generic NFT receiver is great, using interfaces is very nice ( I am assuming some pre/post conditions will be added ) Actually just knowing the address and nft type / id would allow me to borrow from standard path would be a dream.

If there will be a generic NFT storage (collection), transactions will be easier to read, less boilerplate code.

Any plans to add a standard Collection along side the standard?

2 Likes

Thanks for submitting this! Definitely some things I like in here, but I would like to focus in on some major security concerns I have in case they have already been discussed but I’m not aware. Primarily around the shared generic receiver. It is my belief that encouraging this in the standard is not a good idea and will open up two main security issues:

  1. The blast radius of leaking a provider to this collection is vast. One developer doing the wrong thing for their dapp and exposing a provider means that everything in the universal receiver is now at risk. Devs will see a universal receiver and they will make use because it will be easier than doing it themselves. So over time that is likely going to be the defacto place that things are stored. How do we prevent the leaking of a provider from leading to wiping an account of everything it owns? Before this, a leaked provider would mean one collection is at risk.
  2. A universal receiver means we are introducing a very easily executable DOS attack. With self-deployments coming, a bad actor could make a contract that spams large NFTs into any wallet that has configured it and start to drain people of their flow. They could even prevent the destruction of the malicious assets by throwing a panic or failing a precondition on the destruction of the nft theoretically. In that scenario, the storage fees taken up by that attack are lost to the victim forever.

Broadly speaking, I think that the benefits of a universal receiver are not sufficient to outweigh these two big risks and I would rather them not be included until these are solved.

3 Likes

Great point! Nothing in this proposal would stop projects from defining their own collection if they wanted to, but it would just not make it mandatory any more. They could still even use the generic public CollectionPublic interface for their custom collection if they wanted. The single shared collection would just be a convenience that projects could use if they wanted. That part of it is definitely still up for debate though if the core proposal gets good feedback.

1 Like

I’ll try to address @jacob and @austin’s comments at the same time.

Supporting multiple NFTs per contract just gives developers more flexibility in how they manage their contracts. Managing a single contract is much easier than managing multiple. For example, if I wanted to create a racing game with drivers, cars, and car parts/memorabilia as NFTs in addition to rules about how they interact, I’d rather do that from within a single contract. I think it makes the code cleaner and easier to manage. It also better aligns with the current expectations for token standards on other blockchains, such as ERC-1155 on ethereum.

Addressing the DOS attack concern, I made a comment about that in the proposal. The code that I showed here is definitely not what we would expect the final version to look like. We would definitely need to put some tools in for the user to specify which NFT types they are willing to receive in their collection so that they don’t get spammed.

As for the provider concern, the same comment applies. I definitely think that a provider to the generic collection would need to specify which NFT type it is valid for, and also have the option to specify the specific IDs that it is valid for.

I believe that getting creative with the providers and receivers sufficiently addresses those concerns, but I would definitely like to hear more from y’all if you think that there is still a risk in those regards. I can update the sample code to reflect how I think those providers and receivers might work.

@jacob Your point about discoverability and checking in multiple places is a good one, and we may not have fully considered projects who want to use their own collections. Like bluesign said, using a generic collection removes a ton of boilerplate code and also would let anyone use a generic transaction to send NFTs because the specific types and such don’t need to be included unless someone is doing something different. Also, we could potentially include something in the generic collection that allows people who still use custom collections to link the custom collection to the generic collection so that transactions and scripts still only have to go to one place to discover the NFTs in an account.

Sounds like the generic collection is a big sticking point for a lot of people. There are definitely a lot of things to discuss there once we start actually building the real standard, but that part is not critical to the core proposal, so it can definitely be removed while retaining the core proposal here if there is enough push back.

1 Like

I have played around with a AuthPointer struct that is simply a wrapper around a Provider and a single id. The struct allows you to withdraw only this single NFT and nothing else. Having the generic collection have a factory method to create those could be an option. Or even better have it be a method on authAccount you can call since then it is a lot safer.

If anybody is interested the code is here, allthough it is not final. https://github.com/findonflow/find/blob/v2/contracts/FindViews.cdc#L199

Regarding the universal collection I would just like to point out that it solves a HUGE problem. That is the initialization of storage slots for a given type. Right now solving this in a generic way and be compatible with dapper wallet/blocto transaction hash checking is a huge pain. Allthough amit has recently suggested some changes that will help out here.

2 Likes

Thanks @flowjosh! Appreciate the feedback, will do my best to address here:

I think a lever of some kind for what the generic receiver can take makes sense. That does take away some of the benefits to this, though. Things like airdrops aren’t possible (redeeming is though), and it also could open up a vector for a malicious dapp or user to revoke permissions for a given type and bricks parts of the account. That can lead to some really weird use cases like for flowty when trying to close out loans (but this is already an issue in the current setup.) One really important piece you’d also need is some kind of proxy that can hold things for users until they decide to support them (@bluesign and I have been working on something for this).

Those kinds of restrictions make sense to me and definitely go a long way in addressing our risk vectors. The provider is basically akin to approval for a specific item versus approval for all on EVMs. I think as a concept that should apply to all providers and ideally there’s an equivalent to FT vaults as well where you have a provider that can only withdraw a specific number of tokens.

One other concern did pop up in my head overnight, though, which I think is just inherent to this design. Would we throw away the keys to this contract once it’s deployed or at some point in the future? You have the risk that someone can gain access to the account that owns this universal collection contract and start to do some very widespread harm, although maybe at that point I’m just a doomsayer and reaching for straws.

Now these two are quite interesting to me because I’d much rather this as the default approach. Basically a standardized way to ask a contract to set itself up with a given AuthAccount which could then make a boilerplate collection for a given type or types from this helper contract and register it to this generic receiver. If you did that, the generic receiver is more like a proxy than anything else. Collections would still exist elsewhere, but I can access it all from one place. And if I find that a collection doesn’t exist yet, I can ask the AuthAccount to prepare it for me and then I’m good to go. The main issue I see there is that it means a collection has to be setup to receive it, but that already must be true in some way to prevent the DOS risk anyway. So to ensure something can receive a certain item, you already must have access to the AuthAccount to enable it.

1 Like

I think GenericCollection should be generic collection and store everything in user’s account. We don’t need to do it proxy etc.

What we can do is in my opinion, refine access for Provider and Receiver interfaces.

Consider below:

   pub struct AllowedTypeProvider : Provider{
		access(self) let cap: Capability<{NonFungibleToken.Provider}>
		pub let allowedTypes: [Type]
		
		init(_ cap: Capability<{NonFungibleToken.Provider}>, types: [Type]) {
			self.cap=cap
			allowedTypes =nftType
		}
        pub fun withdraw(type: Type, withdrawID: UInt64): @{NFT} {
            pre{
                allowedTypes.Contains(type) : "Type is not allowed for withdrawal"
            }
             return <- self.cap.withdraw(type, withdrawID)
        }

        pub fun transfer(type: Type, withdrawID: UInt64, recipient: &AnyResource{Receiver})
            pre{
                allowedTypes.Contains(type) : "Type is not allowed for withdrawal"
            }
            ........
        }
    }

    pub struct AllowedIdProvider : Provider{
		access(self) let cap: Capability<{NonFungibleToken.Provider}>
		pub let allowedIds: [UInt64]
		
		init(_ cap: Capability<{NonFungibleToken.Provider}>, ids: [UInt64]) {
			self.cap=cap
			self.allowedIds=nftType
		}

         pub fun withdraw(type: Type, withdrawID: UInt64): @{NFT} {
              pre{
                allowedIds.Contains(withdrawID) : "ID is not allowed for withdrawal"
            }
            .....
         }

         pub fun transfer(type: Type, withdrawID: UInt64, recipient: &AnyResource{Receiver})
              pre{
                allowedIds.Contains(withdrawID) : "ID is not allowed for withdrawal"
            }
             ...
         }
       
    }

then we can use them like chained or separate. F

    var cap = AllowedIdProvider(AllowedTypeProvider(getCapability(/storage/genericCollection), types: ...), ids: ...)

Possibilities are very great here:

For receiver:

We can do AllowedTypeReceiver, ( even we can add something like if it is not in allowed types, put it to some another storage with hard limit on storage )

1 Like

Putting some more thoughts after sleeping on the shared collection idea

I am generally not in favor of one universal collection, especially as part of the standard. I think it will segment NFTs that need custom code in their collections away from those that don’t, and it opens up a risk that if the contract which represents the shared collection ever has an issue (or is maliciously upgraded), then theoretically an entire wallets NFT inventory is compromised, not just that one collection. Scoped providers help, they may even solve it when framed by the current exploits we can think of, but storing things all together is, in my opinion, inviting bad actors to try to figure out how to get to this central place. It’s a huge risk to put everything into one storage location. I think it also gives way too much control to whatever entity owns this contract. A proxy means that if something goes wrong, it can be removed and the ecosystem can adapt around it not being usable anymore. Actually putting things in storage on this shared location would prevent us from going around it, we would be completely reliant on it never being corrupted and the ecosystem will be dependent on this seemingly forever. It also could lead to custom collections being treated as second-class citizens in terms of discovery. Dapps will reasonably start with support for the shared collection which means other ones will have to ask for custom code for themselves to be supported. I think most of us have seen how slow that can be, there’s always more to build and so it would be a constant uphill battle with each dapp also repeating much of the same work. That kind of friction isn’t good, it might lead to devs not making things the way they want or need because they wouldn’t be as accessible. It will be a self-fulfilling cycle.

A proxy still gives us the benefits of bringing things together for discoverability/ease of transacting while at the same time:

  1. Storing things in separate paths which should protect them from things going wrong at least more so than one central place.
  2. Not playing favorites to an individual’s selected implementation (boilerplate collection or custom)
  3. Letting the community build better options in the future if they want, without having to perform some extensive migration of move off this new critical piece of infrastructure

The proposal for scoped providers are, I think, a great value-add either way. Providers as of now have too many permissions and so adding them in as a means to reduce what’s accessible is really important. As an example, when Flowty automatically repays a loan, it uses a stored provider to the borrower’s vault and only withdraws the needed amount. It would definitely be much safer to only be able to withdraw a specific amount to avoid malicious updates where the withdraw amount attempts to drain the vault our provider references.

3 Likes

I agree a lot with what @austin has pointed out here. I would also just further stress the strains this puts on discoverability.

Right now for example, I have implemented a Discord bot that verifies users assets. It’s actually quite simple because I can just check the Collection associated with the project (for example, Flovatar.Collection) and see if a user has any NFTs using the standardized getIDs() function. Now, the process would be not only be checking the generic collection, but trying desperately to discover NFTs in custom collections that aren’t enforced by a standard anymore.

Even worse, this new standard wouldn’t require developers to implement their own collections in a correct way. Because if we’re being honest, most NFT projects will define their own collections if they are meaningful, since they will do certain actions that aren’t covered in the generic collection. If anything, I think enforcing this as a standard would only encourage developers to mess up their collections by not naming their Collection resources properly, not implementing CollectionPublic or Receiver interfaces, not implementing correct borrow functions, and just defining their collections in their own unique way which doesn’t standardize NFT discovery at all. For example one project could name their borrow function borrowNFT while another one could name it borrowToken, and then we have no way to discover the NFT more broadly.

I think a generic collection is a good idea, and something that is needed in Flow ecosystem. Maybe we even make it a default in account storages like FlowToken? I just think that making it a standard doesn’t make sense since it only de-standardizes nearly all discovery.

2 Likes

Great post!

I would like to show my support for the ability to have multiple NFTs per contract. I’ve spent the last few days trying to figure out a workaround for this limitation and avoid creating a new contract only to mint different NFTs. Also posted on discord last week about this. This would be of great use to me and will simplify my current code, for example.

Regarding the generic collection, I can’t decide my position on it, but I think I would still prefer to use specific collections for my projects. That said, as it’s not a mandatory feature, this could become valuable in the future as new applications and use cases are developed.

1 Like

How does everyone feel in particular about the proposal of removing the Nested Type Requirements feature from Cadence?

1 Like

It would be great to make borrowNFT return &NFT?. This would allow users to determine if the collection contains the NFT with the given ID, or not.

3 Likes

Nest Type Requirements are what hinder multiple FT or NFT types per contract, right? How will we ensure we are able to access each type from a dapp perspective? Sounds like allowing multiple types will complicate things on that front quite a bit

1 Like

Technically do we have solid proof that this is abused? I mean except FT/NFT standard, I have never seen it used.

Interfaces are not enough most of the time and tbh nested type requirements are the reason we don’t have big exploitable contracts. It is taking away big foot gun from developer. ( Though in my opinion little ugly )

But why do we want to remove a developed language feature ?

1 Like

The use of nested type requirements in the FT/NFT standards limit to one concrete FT (vault) and NFT per contract, correct.

Changing the standards not to use nested type requirements, and removing nested type requirements from the language, should really have no effect on the existing use, nesting interfaces and referring to them would still be supported. The only real change would be that this would “decouple” the Vault and NFT interfaces from their contracts: These interfaces would still be nested in their standard contracts, but an implementation will not be required anymore (through the nested type requirement feature) to provide exactly one concrete instance conforming to the interface anymore, it can now have multiple.

1 Like