Hey everyone, Iām working on a series of blog posts about how Cadence fixes a lot of the Solidity problems that caused hacks in the past and Iām looking for some feedback about how readable it is, how we treat Solidity, and how effective the messaging is. Weāll be posting this and a lot more like it in the near future to get more marketing out there about cadence.
Title (WIP): Your choice of smart contract programming language matters more than you think
Welcome to my series of blogs about common and high-profile vulnerabilities in Solidity smart contracts. In this series, Iāll explain how different vulnerabilities have been exploited in Solidity smart contracts and how choosing languages like Solidity actually sets developers up for difficulty from the beginning. Solidity requires developers to learn all of the extra coding patterns and best practices to write secure code. These coding patterns and best practices are difficult to understand and implement while keeping your contract performant and clearā¦ You canāt trust all developers to do that. Developers should get to focus on building awesome experiences instead of fixing security footguns. I would know. Iām one of them.
Iāve been in the web3 smart contract world since early 2017, establishing myself as a solidity developer and security auditor while working on projects like modular libraries and the first interactive ICO implementation. Iāve been around for most of the hacks that have happened to smart contracts so I know exactly how it can negatively affect users and developers. I joined the Flow team in 2019 and have been working on designing more secure smart contracts experiences ever since.
The DAO Hack
The first hack Iāll discuss is āThe DAOā, a significant event in the history of cryptocurrency. The DAO, or Decentralized Autonomous Organization, was a blockchain-based investment fund that raised $150 million in Ether through a token sale in 2016. The ETH was held in a smart contract that was collectively managed by token holders. Unfortunately, the DAO was hacked, and the hacker managed to drain most of the funds from the smart contract. The hack used what is now a well known āreentrancy attack,ā where the hacker recursively withdrew funds from the smart contract without their balance being updated.
Here is a basic example of a Solidity contract that can be hacked with reentrancy. The DAO was slightly more complex, but the principle is the same.
Contract GroupEtherBank {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
// make sure the caller has enough balance to withdraw
require(balances[msg.sender] >= _weiToWithdraw);
// make sure they arenāt withdrawing more than the limit
require(_weiToWithdraw <= withdrawalLimit);
// Make sure they have waited long enough to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
// Send the Ether to the caller
require(msg.sender.call.value(_weiToWithdraw)());
// Update the callerās balance and last withdraw time
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
}
}
The withdraw method first validates that the caller is authorized to withdraw their tokens before sending the ETH with msg.sender.call and updating the balances in the contract. Seems fine, right? Nope! I told you not to trust me!
The vulnerability lies in Solidityās strange built-in fallback function.
The fallback function is a function that can be included in a smart contract that executes some code when ether is sent to it without a corresponding function call.
contract FallbackExample {
boolean public calledFallbackFunction;
fallback() external payable {
calledFallbackFunction = true;
}
}
Fallback functions have to exist because Ether can be sent to an address that isnāt able to manage it, so many contracts will just forward the Ether to a different user address.
Creative use of fallback functions has been the vector by which many Solidity smart contracts have been hacked.
The contract that got hacked doesnāt have a fallback function, but the contract that hacks it does. In the Bank contractās withdraw function, the balances and other values are updated after the Ether is sent instead of before.
require(msg.sender.call.value(_weiToWithdraw)());
// Update the callerās balance and last withdraw time
balances[msg.sender] -= _weiToWithdraw;
The attacker deployed a smart contract that masqueraded as an āinvestorā and deposited some ETH into the DAO. This entitled the hacker to later call the withdraw()
function in the DAOās smart contract. When the withdraw()
function was eventually called, the DAOās contract sent ETH to the hacker. However, the hackerās smart contract intentionally did not have a receive()
function, so when it received ETH from the withdraw request, the hackerās fallback function got triggered. This allowed the hackerās fallback function to repeatedly call the withdraw method to withdraw funds from the DAO contract. Since the send happens before the balance is updated, it never is reached, and all the tokens get withdrawn from the DAO.
Reentrancy Attacks
From Reentrancy Attack in Smart Contracts - GeeksforGeeks
Reentrancy comes in various forms and is a common vulnerability of smart contracts, and it can exist in smart contracts on various blockchain platforms, other languages such as Vyper and Rust are also susceptible to this problem. Of course, you can just tell developers to use a correct coding pattern like moving the balance update to before the Ether transfer to avoid it. But as weāve established, most developers are lazy and canāt be trusted. Case in point, I wrote a paragraph of this blog using an AI. Try to guess which one! Just kidding, pay attention to the actual content! (Weāre also easily distracted)
Fortunately, smart contract engineering standards have improved a lot since 2016 and there is a new programming paradigm called resource-oriented programming that helps fix many of these problems, as well has a greater reliance on a strong type system and other improvements that were not obvious back then. Resource oriented languages like Cadence and Move have emerged in recent years for blockchains like Flow, Aptos, and Sui and make smart contract developers lives much easier.
Solving Reentrancy with Cadence
How does a resource oriented programming language like Cadence help lazy developers like me avoid vulnerabilities like the DAO hack? As youāll see in most of the articles in this series, the second you start developing in Cadence, you gain a superpower. It doesnāt require any extra work besides learning the language. It just comes by default. That superpower is that in order to avoid these vulnerabilities, you are not required to have read an obscure corner of a documentation page for every feature or use specific safe libraries. You get security and safety simply because you are using a better language.
I know, who would have thought that some languages are just better suited to different domains than others, especially when they are built with 10 years of knowledge gained from using the other ones!
In a strongly typed resource-oriented language like Cadence, security protections are built-in to the language itself, so a developer would have to actively intend to build most of these vulnerabilities into their smart contracts to be susceptible to them. This allows developers to focus on what truly matters, a great experience for their users.
First of all, Cadence doesnāt have a fallback function, so this exact version of this vulnerability is not even possible to begin, but even if there was something like it, Cadence has additional protections to make it safer for developers and users.
Let me explain with an example of how this Group Bank smart contract would work in Cadence. Iāll use simplified Cadence code to make this easier to understand for newcomers.
The biggest difference between a language like Solidity and resource oriented languages is how ownership of assets is tracked. In most languages, including Solidity, ownership is tracked via a ledger in a central location. When you transfer tokens, your balance is reduced in the ledger and the address you are transferring tokens to has its balance increased.
In a resource oriented language, assets are actually represented as objects, called resources, that are directly stored in your accountās storage. This is much more analogous to the real world because you can think of assets in a resource oriented language like physical objects or cash that you store in your house or your purse, respectively. When you want to transfer ownership of the assets, you literally move them from your accountās storage to the account storage of the person you are transferring to. Even MORE decentralization!
Comparing assets to how they work in the real world is actually helpful when thinking about many different aspects of designing smart contracts in Cadence!
What does this have to do with preventing re-entrancy, you might ask?
An important consideration is that with this model, each asset stores its own metadata, including balance. So an object in Cadence that tracks a fungible token balance, like Ether, would look something like this:
access(all) resource Vault {
access(all) var balance: UFix64
}
Just like a cash bill has a number on it that says how much it is worth, each Vault has a balance field on it that indicates how much it is worth. But, unlike real cash, I can combine multiple Vaults into a single vault that has a sum of the combined balances! You unfortunately cannot do that by smashing two bills together. Trust me, Iāve tried.
The same works in reverse, when transferring tokens, one vault is split into two vaults so you can keep the tokens that you do not transfer, and the second vault can be moved to a different account.
Disclaimer: Do not try to reproduce in real life by ripping a cash bill in half.
Here is the function, defined within the Vault resource that is called to achieve this:
// Only the person who literally has possession of the Vault object can call this
access(Withdrawable) fun withdraw(amount: UFix64): @Vault {
self.balance = self.balance - amount
return <-create Vault(balance: amount)
}
Compare that to the withdraw function in the solidity contract above:
// Two operations are required to move tokens
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
As you can see in our Cadence code, since we actually have to return the withdrawn Vault in the return statement of the function, there is no way for a function to re-enter the withdraw method. Additionally, there is a built in pre-condition to the fungible token standard that requires that there has to be enough balance to withdraw, so that would also check and fail the withdraw condition before the attacker is able to drain all the funds.
Bank Contract in Cadence
Here is how a simple Bank like the one above could be built in Cadence:
access(all) contract GroupFLOWBank {
/// Dictionary (Mapping) that stores each users' deposits
/// Key: The ID of the user who has deposited
/// Value: The Vault that the user deposited
access(contract) let deposits: @{UInt64: FlowToken.Vault}
/// Any user can call deposit to store their vault here
/// They get a resource object back that they can use to withdraw their deposit
access(all) fun deposit(from: @FlowToken.Vault): @DepositManager {
let manager <- create DepositManager()
// Store the depositor's vault at the UUID of their DepositManager resource
self.deposits[manager.uuid] <- from
// Return their manager to them
return <-manager
}
/// Object that the owner of a deposited vault stores in their private account
/// that gives only them access to only the vault that they deposited
access(all) resource DepositManager {
access(all) fun withdraw(amount: UFix64): @FlowToken.Vault {
// Get a reference to the vault that they deposited
let depositedVaultRef = (&GroupFLOWBank.deposits[self.uuid] as &FlowToken.Vault?)!
// Return the amount of FLOW they want to withdraw
return depositedVaultRef.withdraw(amount: amount)
}
}
This might be a little confusing to you if youāve never seen a resource-oriented programming language before. In a Cadence contract, most functionality is defined within resources, so it is not public by default because resources are stored in an accountās private storage.
In the Bank contract, each userās deposited tokens are stored in their own vault instead of a global pool and only they can call their withdraw method because the withdraw method is defined in the resource object that they store in their private account storage, not in a public function like it is in Solidity! Also, since the withdraw method is tied to the resource, it can only access the corresponding Vault stored with their resourceās UUID. Additionally, because the reduction of the balance is integral to the withdrawal process, a reentrant function could not call the withdraw method again because the tokens have already been subtracted and moved away. The malicious withdraw would simply fail.
Conclusion
I hope after reading this, you can understand a little more about why resource-oriented programming is a safer and more robust way to build smart contracts. The programming languages we use should help protect us from potential problems, not leave us helpless to make simple coding mistakes that could cost us millions of dollars. Iāve only scratched the surface of what resource-oriented programming makes possible, but Iāll be exploring more in future posts, so subscribe and stay tuned!
I highly recommend checking out the Cadence documentation and tutorials to learn more about this programming paradigm. See you next time!