Quoting the official documentation, Solidity “is a contract-oriented, high-level language for implementing smart contracts.” It was proposed back in 2014 by Gavin Wood and developed by several people, most of them being core contributors to the Ethereum platform, to enable writing smart contracts on blockchain platforms such as Ethereum.
Solidity was designed around the ECMAScript syntax to make something web developers would be familiar with, but it is statically typed like C++, with support for inheritance, libraries, and user-defined data types.
At the time Solidity was proposed, it had significant differences to other languages also targeting the EVM (e.g., Serpent, LLL, Viper, and Mutan) such as mappings, structs, inheritance, and even a natural language specification NatSpec.
Like other programming languages targeting a Virtual Machine (VM), Solidity is compiled into bytecode using a compiler: solc.
Smart Contracts can be seen as a computer protocol intended to complete some task according to the contract rules. In the cryptocurrencies context, smart contracts enforce transactions’ traceability and irreversibility, avoiding the need of a third-party regulator like banks. This concept was suggested by Nick Szabo back in 1994.
This article is an introduction to Solidity from a security standpoint, created by the Checkmarx Security Research Team.
As more and more people/organizations look to blockchain as a promising technology, and being willing to build on top of it, it is mandatory to apply software development best practices such as code review, testing, and auditing while creating smart contracts. These practices become even more critical as smart contracts execution happens in public with source code generally available.
It is hard to ensure that software can’t be used in a way that was not anticipated, so it is essential to be aware of the most common issues as well as the exploitability of the environment where the smart contract runs on. An exploit may not target the smart contract itself, but the compiler or the virtual machine (e.g., EVM) instead.
We cover that in the next sections, providing a Proof-of-Concept that demonstrates the discussed topics.
In the context of Ethereum (abbreviated Eth), Smart Contracts are scripts that can handle money. These contracts are enforced and certified by Miners (multiple computers) who are responsible for adding a transaction (execution of a Smart Contract or payment of cryptocurrency) to a public ledger (a block). Multiple blocks are called blockchain.
Miners spend “Gas” to do their work (e.g., publish a smart contract, run a smart contract function, or transfer money between accounts). This “Gas” is paid using Eth.
In Solidity, private may be far from what you may expect, mainly if you’re used to Object-Oriented Programming using languages like Java.
A private variable doesn’t mean that someone can’t read its content, it just means that it can be accessed only from within the contract. You should remember that the blockchain is stored on many computers, making it possible for others to see what’s stored in such “private” variables.
Note that private functions are not inherited by other contracts. To enable private functions inheritance, Solidity offers the internal keyword.
Preventing functions from reading the state at the level of the EVM is not possible, but it is possible to prevent them from writing to the state ( i.e., view can be enforced at the EVM level, while pure cannot).
The compiler started enforcing that pure is not reading the state in version 0.4.17.
Reentrancy is a well-known computing concept, and also the cause of a $70M hack back in June 2016 called the DAO (Decentralized Autonomous Organization) Attack. David Siegel authored “Understanding The DAO Hack for Journalists” a complete events timeline and comprehensive explanation of what happened.
“In computing, a computer program or subroutine is called reentrant if it can be interrupted in the middle of its execution and then safely be called again (“re-entered”) before its previous invocations complete execution.” (Wikipedia).
By using a common computing pattern, it was possible to exploit a Smart Contract. It is still possible. The call() function is the heart of this attack, and it is worth noting that it:
- is used to invoke a function in the same contract (or of another contract) to transfer data or Ethereum;
- does not throw, it just returns true/false;
- triggers the execution of code and spends all the available Gas for this purpose; there’s no Gas limit unless we specify one;
The following warning message was taken from Solidity’s documentation:
“Any interaction with another contract imposes a potential danger, especially if the source code of the contract is not known in advance. The current contract hands over control to the called contract and that may potentially do just about anything. Even if the called contract inherits from a known parent contract, the inheriting contract is only required to have a correct interface. The implementation of the contract, however, can be completely arbitrary and thus, pose a danger. In addition, be prepared in case it calls into other contracts of your system or even back into the calling contract before the first call returns. This means that the called contract can change state variables of the calling contract via its functions. Write your functions in a way that, for example, calls to external functions happen after any changes to state variables in your contract so your contract is not vulnerable to a reentrancy exploit.”
The highlighted part in bold text above is exactly how a Smart Contract can be exploited due to Reentrancy. In the Proof-of-Concept section below and in the accompanying video, there’s a ready to run example. To avoid this attack:
- “be prepared” – any function running external code is a threat;
- These functions:
<address>.transfer(uint256 amount)/ <address>.send(uint256 amount) return (bool)
are safe against Reentrancy as they currently have a limit of 2300 Gas;
- if you cannot avoid using call(), update the internal state before making an external call.
Solidity data types are cumbersome because of the 256 bits Virtual Machine (EVM). The language does not offer a floating point representation and data types shorter than 32 bytes are packed together into the same 32 bytes slot. The literal 0 type-infers to byte, not an int as we might expect.
Being limited to 256 bits, overflow and underflow are something we may expect. It can happen with a uint8 whose max value is 255 (2ˆ8-1 or 11111111)
OverflowUint8.sol source code or with a uint256 whose max value is 1.157920892×10 (2ˆ256-1)
OverflowUint256.sol source code
Although uint256 is suggested to be (more) secure as it is unlikely to overflow, it has the same problem than any other data type. The batchOverflow bu (CVE-2018–10299) is a great example of a uint256 overflow.
Consider the following Bank Smart Contract which keeps tracking of balances for addresses that put ether on it.
A careful look at the withdraw() function reveals the reentrancy pattern highlighted in the above correspondent Common Issues > Reentrancy section external call before internal state update.
Now we need a malicious crafted Smart Contract to exploit the Bank one: Thief
Let’s rehearse the robbery using a Solidity development environment. To run it we’ll just need a docker enabled environment.
Clone the solidity-ddenv project and move inside the solidity-ddenv folder
$ git clone https://github.com/Checkmarx/solidity-ddenv && cd solidity-ddenv
Let’s start the development environment
Creating network “solidityddenv_default” with the default driver
Creating ganache … done
Creating truffle … done
If ddenv started correctly you’re expected to be inside workspace folder (you can check it running pwd).
Let’s move into reentrancy directory where the Bank and Thief Smart Contracts are located
$ cd reentrancy
Now, it’s time to compile the source code
$ ddenv truffle compile
Starting ganache … done
Compiling ./contracts/Thief.sol… Writing artifacts to ./build/contracts
and deploy the Smart Contracts to our development network:
We are now ready to perpetrate the attack. Let’s spawn a console to our development network so that we can issue a few commands
$ ddenv truffle console –network development
Starting ganache … done
truffle(development) > is the prompt. If you want to run the attack yourself, just copy the commands next to the prompt from the scripts below and paste them into the console prompt you have launched before.
Discovering vulnerabilities like the one mentioned above is why the Checkmarx Security Research team performs investigations. This type of research activity is part of their ongoing efforts to drive the necessary changes in software security practices among organizations worldwide.