How to do Token Transfer Events Properly
Part 1 in a series about the new Cadence token smart contract standards on the Flow Blockchain
Flashback to November 2019. COVID was still relatively unknown, NBA Top Shot was in early development, and a bored ape was simply something that made you a little bit sad when you saw it at the zoo.
I had started working on the Flow Blockchain team a few months earlier and I was still adjusting to the reality that, after being hired for a Solidity developer role, I was instead working with this strange new programming language called Cadence! All I had to work with at that point was the formal language specification and about 50 lines of fungible token pseudo-code written by Bastian and Dete, the original designers of Cadence. I think by then I had wrapped my head around what resources and capabilities were and my mind was continuously being blown by all the implications.
(If it isn’t clear, I am very happy that I got to be a Cadence developer instead of a Solidity developer) ;)
I’m sure many of you who understand Cadence can relate to the experience of finally understanding resource-oriented programming and capability security. It was a pretty exciting moment for all of us, regardless of when it happened.
I was in a bit of a hurry too, because while I was experiencing these successive mind-blowing realizations, I was also under some pressure to work with the NBA Top Shot team to write the Top Shot smart contract which would be the first production smart contract ever written in Cadence. This also required that we design the Fungible Token and Non-Fungible Token standards at the same time.
I was woefully unprepared to do this on my own, but thankfully the Flow team has some of the smartest smart contract programming language minds in the world, so we iterated for a few months on it and deployed them to testnet and mainnet when Flow mainnet launched in June 2020.
All things considered, I think we did a darn fine job of it and it has proven to be very useful in the following years. Also, Flow has been around for almost three years and as far as I know, there have not been any smart contract code vulnerabilities exploited on mainnet in that time. That is a testament to first and foremost the power and safety of Cadence as a language but also to the strength of the Flow and Cadence community collaborating on smart contract design, coding standards, and reviews. If you’re at all part of this, bravo! You’re part of something special. If you want to be part of it, join the Flow Discord to get started.
As with every creative endeavor, we and many of the wonderful Flow community members have discovered some parts of it that we wish we could have done differently.
This is why we’ve shared proposals for upgrades to both the Fungible Token standard and the Non-Fungible Token standard. I posted a blog last year about the initial proposal which is still super valuable to read, but now we have FLIPs and code proposals that are nearing their final forms and we are really excited about it!
In this series of blog posts, I want to go into detail about some of the changes, why they are important, and what they enable, because while it might be difficult to understand at first, there are some pretty important improvements coming that will hopefully help Cadence developers for years to come.
This post is about Cadence, the resource oriented programming language for the Flow blockchain. Take a look at the introduction if you are unfamiliar.
Standardizing Events
Events are such an under-appreciated part of smart contract design. I could do a whole blog post about proper event design in smart contracts, something I need to do more research on!
I want to take a slightly more focused look at just the events in the token standards today.
Specifically, the humble Withdraw
and Deposit
events, the pack mules of the Flow blockchain. In Cadence, transfers happen in two steps, a withdrawal from an account, and a deposit to another account, so both events are needed. Many off-chain event listeners depend on these events to build a history of account ownership and balance records that can be easily queried and indexed, just like in other blockchains.
Currently, the token standards define standard events in a contract interface:
pub contract interface NonFungibleToken {
pub event Withdraw(id: UInt64, from: Address?)
pub event Deposit(id: UInt64, to: Address?)
}
A specific token smart contract has to implement the contract interface to define a Flow-based fungible token. This event requirement requires that implementing contracts also define these events exactly as they are defined in the standard. The implementing contracts are free to emit them wherever and however they want in their contracts as long as they follow the standard format, like so:
pub contract TopShot: NonFungibleToken {
// Emitted when a moment is withdrawn from a Collection
pub event Withdraw(id: UInt64, from: Address?)
// Emitted when a moment is deposited into a Collection
pub event Deposit(id: UInt64, to: Address?)
}
Hold up, there is a slight problem with this. While the standard enforces what the format of the events should be, it doesn’t enforce that the contracts that emit them actually emit them with the correct values, or even emit them at all! Additionally, event listening software has set up a different event query for each specific contract that it wants to track balance changes for. This can be cumbersome, unreliable, and hard to scale if not managed properly.
In the upgrades to both token standards, the goal is to make it as easy as possible for developers to build their tokens right. Standardizing event emissions is part of this, so in the new standards, the events will now be emitted directly by the standard interfaces instead of by the implementations. This is much safer because now the standard can actually enforce that the correct values are emitted properly for each withdraw and deposit. Also, the events will all come from the same place, the FungibleToken
or the NonFungibleToken
contracts, which simplifies listening to these events.
/// In the `FungibleToken.Vault` resource interface
/// within the `FungibleToken` contract
///
/// The deposit method requirement from the `FungibleToken.Vault` interface
/// 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"
// emit the standard deposit event
FungibleToken.emitDepositEvent(amount: from.getBalance(), to: self.owner?.address, type: from.getType().identifier)
}
post {
self.getBalance() == before(self.getBalance()) + before(from.getBalance()):
"New Vault balance must be the sum of the previous balance and the deposited Vault"
}
}
Now, instead of the event format including the address and name of the specific token contract that is associated with the action, like `A.1654653399040a61.FlowToken.Deposit`, all events will always have the standard format `A.f233dcee88fe0abe.FungibleToken.Deposit`.
In the case of NFTs, instead of, for example, A.0b2a3299cc857e29.TopShot.Deposit, the event format will be A.1d7e57aa55817448.NonFungibleToken.Deposit.
This won’t invalidate old event formats from contracts that were deployed before the upgrade though! Those old events will still be emitted so that old methods for monitoring events will still work. This is purely a new feature.
Now that events are all emitted from the same place, how can we tell the events apart now? Here is the new format:
pub event Deposit(amount: UFix64, to: Address?, type: String) // new type parameter!
In addition to standardizing event emissions, the events also include a parameter to indicate the Type of the Vault or NFT that emitted the event. There also is a possibility for including metadata about the tokens that emitted the event, which is still under discussion and could use more feedback if you want to leave a comment about it! We’ll also discuss it in our next community working group session.
This opens up some benefits that aren’t possible or as easy on other blockchains. Now that event emissions all come from one place. It is trivially easy to be able to monitor the amounts and locations of ALL transfers from ALL tokens on the network from a single place. I’m sure that some creative data scientists are going to have a party with all of the possible ways they can analyze that vast amount of data.
Get me an invite to one of those parties! I’ll bring some snickerdoodles and my curiosity about when the most active times are for these event emissions, or maybe about the average quantities per transfer, organized by token type. For NFTs, maybe it could be something like tracking the popularity of certain metadata views among transferred NFTs or noting if drops from certain projects affect the trade volumes of others. I’m just scratching the surface here. There are a TON of possibilities!
Another benefit is that now that basic events are handled by the standard, projects don’t have to worry about including them in their contracts! The removal of these events saves more than 20 lines of code on average, which doesn’t sound like much, but compared to a ~200 line smart contract, that is about one tenth of the code. EFFICIENCY. 🚀
Apart from satisfying my need for speed, this change is a small example of one of the core design goals of these new standards, as well as with Flow and Cadence as a whole:
Developers should not have to worry about pointless trivialities like worrying about network performance, absurd gas fees, or (in this case) unnecessarily copying, pasting, and testing boilerplate code. They should have the time and safety to feel comfortable focusing on what makes their project unique.
Cadence is such a unique and powerful programming language that enables so many novel experiences, and Flow is the blockchain that makes it all possible.
What are you going to build? I can’t wait for the event. :)
Resources
Flow Website: https://flow.com/
Flow Discord: https://discord.gg/flow
Flow Developer Portal: https://developers.flow.com/
I’ll be back soon to talk about the removal of nested type requirements, which is one of the biggest improvements in the standards! Stay tuned!