Hi! ๐ My name is Phill and I am Blockchain Tech Lead at The Fabricant and a Community Rep for Flow. You can find me on:
Discord: Phill#1854
Email: fullstackpho@protonmail.com
Github: github.com/ph0ph0
Twitter: twitter.com/fullstackpho
For a curated collection of the best Flow tools, blogs and articles, check out Get The Flow Down! github.com/ph0ph0/Get-The-Flow-Down/blob/ma..
Before we start, I'd like to say a huge thank you to the Flow community, the Emerald City community, Jacob Tucker, Bjartek, bz, liobrasil, Andrea, and everyone else that was involved in the interesting conversations that were had about this vulnerability!
Introduction
In November 2022 Matrix Market announced that they had identified a vulnerability and that all users should visit the site to apply a security patch.
Blocto also made an announcement, and there was confusion in the community as to what had happened and how it was being fixed.
In this quick article, we will have a look at what caused the vulnerability, why it was an issue, and what we can do to avoid this risk in our code. Let's get to it!
The Vulnerability
The vulnerability itself was not an issue with a contract, but a mistake in a transaction. Below is a link to one of the actual offending transactions:
Matrix Market Vulnerability Tx
The fact that it was a transaction that was causing an issue and not a contract highlights a very pertinent point with building blockchain applications - the transactions that we write can also be unintentionally dangerous (or intentionally if you are a hacker!), and so we should take as much care when writing them as we do with our smart contracts.
The transaction was a type known as an 'initialization transaction'. These types of operations are used to set up a user's account so that it has the resources and links that it needs to operate within a blockchain ecosystem. For example, developers use them to put Collection
resources in a user's account so they can receive NFTs, NFTStorefronts
so that they can make listings on marketplaces, and Vaults
so that they can store fungible tokens.
The issue with the Matrix Market transaction was that during initialization, it had incorrectly linked Vault
resources in the user's account. The incorrect code from the transaction is shown below:
transaction {
prepare(acct: AuthAccount) {
// init Flow
if !acct.getCapability<&{FungibleToken.Balance}>(/public/flowTokenBalance).check(){
acct.unlink(/public/flowTokenBalance)
acct.link<&{FungibleToken.Balance}>(/public/flowTokenBalance, target: /storage/flowTokenVault)
}
if !acct.getCapability<&FungibleToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver).check(){
acct.unlink(/public/flowTokenReceiver)
acct.link<&FlowToken.Vault>(/public/flowTokenReceiver, target: /storage/flowTokenVault)
}
// init FUSD
if acct.borrow<&FungibleToken.Vault>(from: /storage/fusdVault) == nil {
acct.save(<- FUSD.createEmptyVault(), to: /storage/fusdVault)
}
if !acct.getCapability<&FUSD.Vault{FungibleToken.Balance}>(/public/fusdBalance).check(){
acct.unlink(/public/fusdBalance)
acct.link<&FUSD.Vault{FungibleToken.Balance}>(/public/fusdBalance, target: /storage/fusdVault)
}
if !acct.getCapability<&FungibleToken.Vault{FungibleToken.Receiver}>(/public/fusdReceiver).check(){
acct.unlink(/public/fusdReceiver)
acct.link<&FUSD.Vault>(/public/fusdReceiver, target: /storage/fusdVault)
}
...rest of tx...
Can you see what the issue is?
Specifically, it is these lines here:
Found in the second block of //init Flow
acct.link<&FlowToken.Vault>...
Found in the second block of //init FUSD
acct.link<&FUSD.Vault>...
So what is the issue then?
The developer has inadvertently linked the entire Vault
resource to the public path! This means that anyone can get a capability for an unrestricted reference to the Vault
.
In plain language, this allows anyone to withdraw FUSD and FlowToken fungible tokens from the user's account - I think we can be sure that this is not what was intended!
Some of the more eagle-eyed readers will be asking themselves why the Flow token vault was being unlinked and then linked in the first place - the Flow vault is the only fungible token vault that all Flow accounts have linked by default on creation!
How Can We Avoid This?
One of the reasons that we use interfaces in Cadence is to restrict access to composite types (resources and structs: developers.flow.com/cadence/language/compos..). Resources are used in particular to represent 'value' - assets that need to be managed with care, should never be copied (as resources cannot), and are owned by an account. NFTs, Collections and Vaults are resources.
Linking is central to Cadence and the Flow ecosystem. It provides a way for developers to provide scoped access to composite types - exposing only the properties and functions that certain groups of users should have access to in a safe manner.
Vaults store fungible tokens, in the same way that Collections store NFTs. In both of these cases, we need a safe way for other users to deposit fungible tokens into our vaults or NFTs into our Collections, without being able to remove them.
For example, you can see in the Fungible Token standard (github.com/onflow/flow-ft/blob/master/contr..) the Receiver
interface, which allows other users to deposit fungible tokens into another user's account:
pub resource interface Receiver {
/// deposit takes a Vault and deposits it into the implementing resource type
///
pub fun deposit(from: @Vault)
}
When we link resources in Cadence, we almost always want to restrict access to the resource through the use of an interface. What the developer should have done was written the link statements like this:
acct.link<&FlowToken.Vault{FungibleToken.Receiver}>
acct.link<&FUSD.Vault{FungibleToken.Receiver}>
The big difference here is what is contained within the curly brackets - the restriction. This small piece of code translates as "I only want other users to be able to access my Vault using the Receiver interface, which only contains the deposit(from:)
function".
If the developer had done this, then other users could only send Flow and FUSD to these accounts, and not withdraw their entire balance.
By missing this simple short piece of code, other users had access to deposit(from:)
, balance
, and most worrying of all, withdraw(amount:)
!
What Were Matrix Market's Options To Fix This Issue
When the vulnerability was identified, Matrix World sent out a message to their users stating that they should visit the website and apply the security patch. The patch contained an initialization transaction with the correctly restricted Vault
resources. This is definitely the correct approach to take, but could Matrix Market have gone further?
Since Matrix Market knew about the vulnerability and would have had a list of all affected users, they could have exploited the vulnerability themselves and locked the user's tokens. Once the user's funds were safe, they could then contact these users, informing them of the fact that their funds had been safely locked away, and then provide the users with steps to apply the patch and automatically claim their funds back. The Team at Talent Protocol took a similar approach to this (Talent Protocol Hack).
However, locking users' funds has certain ethical implications and connotations within the blockchain community which many may feel contradict the values of the domain.
Hacks that have involved huge sums of money have had a colossal impact on the path and perception of blockchain, and at one time, even caused the forking of the Ethereum network to recover stolen funds (see: help.coinbase.com/en/coinbase/getting-start..).
Platform owners and blockchain engineers have to balance these weights and must always be wary of overstepping the mark. As of the time of writing, Matrix Market has not taken any further action.
So What is the Lesson Here?
As blockchain engineers, we have a huge responsibility to protect our users and their assets.
We do this through a thorough understanding of the blockchain, the language, and the technology. The vast majority of our users will be non-technical, and so they rely on us to employ practices and methodologies in our development process that manages any risk to the end user.
Every single blockchain developer should be reviewing their code with a fine-toothed comb and putting it through its paces using tests, reviews, and where possible, security audits. Linking is so incredibly central to Cadence that it should be handled with extreme care. As mentioned above, a resource should rarely be linked without being restricted by an interface, and so if you ever see this as a user, you should instantly be having second thoughts about signing the transaction!