NBA Top Shot on the blockchain

NBA Top Shot is a first-of-its-kind collectible game that allows people to collect, trade, and sell their favorite NBA highlights as digital tokens. These Moment™ collectibles can be used to complete timed Showcase Challenges to earn exclusive Challenge Rewards, in addition to providing unparalleled and unprecedented access to exciting, real-world experiences -- including our NBA Finals All-Access Experience and 2021 NBA Draft All-Access Experience -- and giveaways that introduces the community and NBA fans to the future of fandom. 

How does it work?

It’s all done on the Flow blockchain, specifically tailored to be fast and scalable for consumer applications. The way we build an application on the blockchain is through a set of Smart Contracts.

It has been almost a year and a half since we first deployed the Top Shot Smart Contracts to the Flow blockchain, and we have learned a lot about Cadence best practices as well as coding patterns in Cadence that we would like to avoid. 

In alignment with these best practices, we have included a few upgrades to some parts of the contract to better reflect the intended design, make data more accessible to other contracts and scripts, and make the contracts more efficient.

Additionally, a bug was reported in the Top Shot Smart Contract that we needed to fix, so we have included the bugfix for this along with the other improvements in a single upgrade, which is the first (and hopefully last) upgrade that will be performed for the Top Shot Smart Contract. 

We recommend that all technically savvy users of Top Shot read about the changes for maximum transparency.

For the full record of the changes, see here.

First Improvement: Make Dictionary and Array Fields Private

In Cadence, fields that are public mean that they are publicly readable but not publicly writable. This only applies to the field itself, but if the field is a dictionary or array, the members of that dictionary or array are still able to be assigned to. See the Cadence best practices document for more info:

Cadence Anti-Patterns

This is an opinionated list of issues that can be improved if they are found in Cadence code intended for production. A…docs.onflow.org

Any Cadence developer should default to making all dictionary and array fields private by default. They should also define explicit getter and setter functions for those that make it extremely clear what type of access the developer wants to allow.

In the case of the Top Shot contract, three fields were changed from `pub` to `access(contract)` or `access(self)`.

// In the Set resource
pub var plays: [UInt32]
pub var retired: {UInt32: Bool}
pub var numberMintedPerPlay: {UInt32: UInt32}

If they remained public, this means that the admin would have the ability to modify the retired status or number minted for specific moment plays, which is not an ability that the admin should have. The Top Shot team has fixed these issues and these fields are now fully immutable.

Second Improvement: Unified Set Metadata Struct

Currently, Sets in Top Shot are represented by two different data structures:

  • A SetData struct, which records the id, name, and series of the set.
  • A Set resource, which records other information about the set, including plays that are in it, editions, and retired statuses. It also acts as an authorization resource for the admin to create editions, mint moments, retire plays, and more.

In addition to these two types, we have added a third struct, `QuerySetData`.

pub struct QuerySetData {
pub let setID: UInt32
pub let name: String
pub let series: UInt32
access(contract) var plays: [UInt32]
access(contract) var retired: {UInt32: Bool}
pub var locked: Bool
access(contract) var numberMintedPerPlay: {UInt32: UInt32}
}

Now, all the information about a particular set can be easily queried by calling the `TopShot.getSetData(setID: UInt32): QuerySetData?` method. This should hopefully make it easier for third party developers to query information about a Top Shot set from a single place.

Third Improvement: Perform state changing operations in admin resources, not in public structs

This issue is the vulnerability reported by a member of the Top Shot community that is now fixed.

In the Top Shot contract specification, when a new play or set is created, the ID tracker for the play or the set is incremented to make sure that the next play or set created would have a unique ID. The contract also emits an event indicating what the new play or set is. The original version of the Top Shot smart contract performed these actions properly, but the problem arose from where these operations used to happen.

These operations were performed in the initializer for the Play and Set structs:

pub struct Play {
init() {             
  self.playID = TopShot.nextPlayID
                          // Increment the ID so that it isn't used again
    TopShot.nextPlayID = TopShot.nextPlayID + UInt32(1)
      emit PlayCreated(id: self.playID, metadata: metadata)
         }
}

At the time, Cadence did not, and still doesn’t, support private struct definitions. This means that anyone could create an instance of this Play struct or the SetData struct whenever they want. Every initialization would increment the id counter and emit an event, even though these sets and plays that were created were not real sets or plays. Eventually, the field would reach the limit for UInt32 and overflow, which could prevent new plays or sets from being created. This would be quite expensive to exploit, but is still a serious vulnerability that should always be avoided.

This references an important security aspect of smart contract development that should always be considered, regardless of the contract you are working on. If you have any operations that change important state in the contract, you should always default those operations to being hidden in an admin resource that restricts that functionality to only those who should be able to do it. There are obviously exceptions to this, but they should be carefully considered before committing to.

Fourth Improvement: Borrow references to resources instead of loading them from storage

The Top Shot contract stores an important resource in a contract field, the Set resource. There are certain functions, including the queryable set metadata functionality that I described above, that need to get information about one of these sets that is stored in the contract. The solution that we originally used for accessing these sets was to load the whole set from storage, read its fields, and then put it back where it came from.

if let setToRead <- TopShot.sets.remove(key: setID) {

// See if the Play is retired from this Set
let retired = setToRead.retired[playID]

// Put the Set back in the contract storage
TopShot.sets[setID] <-! setToRead

// Return the retired status
return retired

}

This works fine, but it is pretty inefficient. It takes a lot of computational resources to load an object from storage and save it again.

This coding pattern also caused a problem with the TopShotShardedCollection.cdc smart contract because when you load a resource from storage, its owner field is set to `nil`. If you try to access its owner field expecting it to be a certain value (like when you are emitting a Deposit event, for example), it will be `nil`, even if you plan to put it right back into storage right after withdrawing it like we did in the sharded collection contract.

The correct solution for this pattern is to borrow a reference to the resource instead. Borrowing a reference only uses only line of code instead of two and is a much more efficient operation:

let collectionRef = &self.collections[bucket] as! &TopShot.Collection

Conclusion

That is it for the upgrades we performed! If you have any additional questions, please reach out in the Top Shot discord and we’ll be able to help you out more.

We’d also like to thank our community contributors for their constant support and feedback.

We’d also like to reiterate that Dapper Labs (and Flow) has a standing bug bounty program. Any auditor who finds any issues or vulnerabilities in our code will receive a significant reward for their efforts.

Interested in building decentralized apps and NFTs as well?

START BUILDING ON FLOW >>>