How to Recreate Broken NFT Collections after Flow Crescendo

Joshua Hannan

--

The year of Crescendo, 2024, will likely go down in blockchain history. Overall, the launch was momentous, culminating a year of anticipation for the Flow community. It included protocol upgrades to improve stability and performance and new Cadence features that unlock even more amazing possibilities for smart contract developers. Celebrating the breakthrough, The Flow World Tour is making a big splash at several worldwide events, where developers consistently choose Flow as the top L1 network to build on. The ecosystem is flying high!

But, it hasn’t been easy getting here though! The Crescendo upgrade was a huge undertaking for everyone and it was extremely impressive what the Flow Community was able to accomplish. There were large breaking changes that came to Cadence and there were a lot of hard decisions to make about these. One of these was the choice to only do the Crescendo migration once for every network. This unfortunately meant that projects that didn’t upgrade their Cadence smart contracts were permanently broken.

We developers are painfully aware that despite our efforts to inform everyone of this reality, there were still projects that missed the memo and woke up on Sept 6th to find their users complaining that the smart contracts no longer worked.

As of now, there is no way to fully recover these broken contracts because the migration required for the data of these projects is too complicated. Fortunately, there is still hope!

Read-only access to broken functionality

We knew that there would be contracts that didn’t upgrade in time, so we introduced this FLIP to recover some of the functionality of broken contracts. We don’t have the authority to allow access to state-changing functions on broken contracts, but we were able to introduce a solution to expose access to read-only functionality for broken Fungible Tokens and Non-Fungible Tokens.

This means that if your FT or NFT contract is broken, you can still query accounts to see which NFTs they own from the old contract. You can use this information to recreate the NFTs that your users own with a newly deployed contract!

Step-by-Step Guide for Recreating a Broken Contract

For those with recovery in mind, regardless of what your plan is, if you’re looking for a starting place for the Cadence 1.0 version of your NFT contract, simply use the Cadence 1.0 version of ExampleNFT as a reference to deploy a new version of your contract. You’ll obviously need to update the contract name, path names, and some type names to match your product. You’ll also need to make any necessary additions and changes to make sure the contract behaves with similar functionality as your old contract.

It is extremely important that you choose a different name for the contract and that you use different path names for your contracts storage and public paths! For example, if your old contract was named JoshCollectibles, I would choose a name like JoshCollectiblesEvolved or JoshCollectiblesV2 for the new contract, and I would make similar changes to the path names for the new contract.

Since you’ll be minting NFTs with a pseudo-random assortment of IDs based on which users decide to claim their NFTs from the old contract, you’ll also need to update the minting functionality to take an ID as an argument so that the newly minted NFT ID has the same ID as the old one. You’ll also need to track which NFT IDs have been minted so that the same ID cannot be minted more than once.

access(all) contract RecoveredNFT: NonFungibleToken {


/// Tracks IDs that have been recovered from a broken contract
access(all) let recoveredIDs: {UInt64: Bool}


/// ...
/// The Rest of the typical NFT contract code
/// ...


access(all) resource NFTMinter {


access(all) fun mintNFT(
id: UInt64,
name: String,
description: String,
): @RecoveredNFT.NFT {
pre {
RecoveredNFT.recoveredIDs[id] == nil || RecoveredNFT.recoveredIDs[id]! == false:
"Cannot recover NFT with ID ".concat(id.toString())
.concat(". This ID has already been recovered!")
}

An important thing to consider is that if you were using UUIDs for your NFT IDs before, the IDs that you’re minting with will no longer be the same as the UUIDs of the underlying resource as before. You’ll need to update any code that you control that makes this assumption. In the example code above, the id field of NFT would be set to id and not uuid. You could still provide the uuid of the old token as the argument to mintNFT().

After having a first draft of your new contract, the first thing that you’ll have to consider is how you want to recreate the ownership of your contract’s NFTs. If you already have an off-chain record of ownership and NFT metadata in a database somewhere, this will make this process way easier for you! If you already know which accounts are supposed to own which NFTs, you can simply ask those users to initialize their accounts (these can be the same accounts as before!) with your new contract’s NFT collection after it is deployed in a new unique storage path. Then, for each user, mint the NFTs that you know they own with the relevant metadata, and transfer those NFTs to their collections.

If you don’t have an off-chain record of ownership, it will be a little bit more difficult, but this is where the NFT program recovery for read-only functions comes in. You’ll be able to use the on-chain record of ownership, just as the NFT gods intended!

Similar to the first example, you’ll need to deploy your contract and then ask any users who want the new copies of their NFTs to initialize their accounts to support your new collections.

This would use the exact same initialization code as you would use for any other NFT smart contract, but you need to make sure that all your path names are different from the original contract!

Then, for each user who wants to claim their NFTs, you’ll need to run a script to query the NFT IDs in each account:

import "NonFungibleToken"


access(all) fun main(address: Address, collectionStoragePath: StoragePath): [UInt64] {
let account = getAuthAccount<auth(BorrowValue) &Account>(address)


let collectionRef = account.storage.borrow<&{NonFungibleToken.Collection}>(
from: collectionStoragePath
) ?? panic("The account ".concat(address.toString()).concat(" does not have a NonFungibleToken Collection at ")
.concat(collectionStoragePath.toString())
.concat(". The account must initialize their account with this collection first!"))


return collectionRef.getIDs()
}

After that, you’ll know which IDs an account owns and you can mint those NFTs as usual and send them to the correct accounts.

Another option would be to allow the users to claim the NFTs themselves. With this option, your users can send a transaction that calls a function on your contract, passing in a reference to their broken collection and their new collection. This function would check to see which NFTs they own and then transfer them the new copies of each NFT, and these new NFTs can be pre-minted or minted at the time of the transaction.

Function:

   /// Function for users to pass reference to their old and new collections
/// to receive new NFTs for each NFT they own
access(all) fun recoverOldNFTs(brokenCollection: &OldNFT.Collection, newCollection: &{NonFungibleToken.Receiver}) {
let ids = brokenCollection.getIDs()


for id in ids {
let NFT <- self.mintNFT(id: id)


newCollection.deposit(<-NFT)
}
}

Transaction:

import "NonFungibleToken"
import "OldNFT"
import "RecoveredNFT"


transaction {


/// Reference to the broken collection
let brokenCollectionRef: &OldNFT.Collection


/// Reference of the collection to deposit the NFT to
let receiverRef: &{NonFungibleToken.Receiver}


prepare(signer: auth(BorrowValue) &Account) {


// borrow a reference to the broken collection
self.brokenCollectionRef = signer.storage.borrow<&OldNFT.Collection>(
from: /storage/oldNFTStoragePath
) ?? panic("The signer does not store an OldNFT Collection object at the path "
.concat("/storage/oldNFTStoragePath. "))


// borrow a public reference to the receivers collection
let receiverCap = signer.capabilities.get<&{NonFungibleToken.Receiver}>(RecoveredNFT.CollectionPublicPath)
self.receiverRef = receiverCap.borrow()
?? panic("The recipient does not have a NonFungibleToken Receiver at "
.concat(RecoveredNFT.CollectionPublicPath.toString())
.concat(" that is capable of receiving an NFT.")
.concat("The recipient must initialize their account with this collection and receiver first!"))
}


execute {


RecoveredNFT.recoverOldNFTs(brokenCollection: self.brokenCollectionRef, newCollection: self.receiverRef)


}
}

As I mentioned before, it is still up to you to ensure that these NFTs have the correct metadata associated with them, and whether that is on-chain or off-chain is at your discretion.

It is unlikely that every single one of your old users will be willing to come to you to retrieve their NFTs, but this will at least be able to help them regain control over their NFTs if they so desire!

I hope this guide was helpful for those of you with broken NFT smart contracts. If any of these instructions are unclear, please reach out to us in the Flow Discord for assistance and we will be happy to help you.

Resources

Flow Website: https://flow.com/

Flow Discord: https://discord.gg/flow

Flow Developer Portal: https://developers.flow.com/

Thanks to Giovanni Sanchez and Jeremy Nation for help writing this!

--

--

No responses yet