How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 1)

Georgios Konstantopoulos
Loom Network
Published in
5 min readJan 8, 2018

--

In a previous post we discussed the future of Ethereum’s scalability by analyzing the concepts presented at Devcon3. Let’s take a moment and imagine that all these scalability issues are now solved, and Ethereum’s Smart Contracts are working without issues.

Are those users going to be good willed or are they possible adversaries who interfere with the smooth functionality of the contracts?

Smart contracts are “immutable”. Once they are deployed, their code is impossible to change, making it impossible to fix any discovered bugs.

In a potential future where whole organizations are governed by smart contract code, there is an immense need for proper security. Past hacks such as TheDAO or this year’s Parity hacks (July, November) have raised developers’ awareness, but we still have a long way to go.

“This is Disneyland for hackers”

In this article we will go through some of the famous security pitfalls and their mitigations.

1. Overflows & Underflows

An overflow is when a number gets incremented above its maximum value. Solidity can handle up to 256 bit numbers (up to 2²⁵⁶-1), so incrementing by 1 would result into 0.

  0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+ 0x000000000000000000000000000000000001
----------------------------------------
= 0x000000000000000000000000000000000000
After reaching the maximum reading, an odometer or trip meter restarts from zero, called odometer rollover.[source]

Likewise, in the inverse case, when the number is unsigned, decrementing will underflow the number, resulting in the maximum possible value.

  0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

You can test the bug here:

As dangerous as both cases are, the underflow case is the more likely to happen, for example in the case where a token holder has X tokens but attempts to spend X+1. If the code does not check for it, the attacker might end up being allowed to spend more tokens than he had and have a maxed out balance.

Mitigation: It has been a standard for a while now to use OpenZeppelin’s SafeMath library.

You can test the fixed bug here:

2. Visibility & delegatecall

For the people who were around in July, this bug will be familiar, after all it was the Parity wallet hack which cost users about 30 million dollars.

Solidity visibility modifiers and their differences.

Public functions can be called by anyone (by functions from inside the contract, by functions from inherited contracts or by outside users)

External functions can only be accessed externally, which means they cannot be called by other functions of the contract. The gist below does not compile, the external visibility of cannotBeCalled does not allow it to be called by the contract’s functions (however it can be called by another contract)

External is cheaper to use because it uses the calldata opcode while public needs to copy all the arguments to memory, as described here.

Private and internal are simpler: private means that the function can only be called from inside the contract, while internal proves a more relaxed restriction allowing contracts that inherit from the parent contract to use that function.

That said, keep your functions private or internal unless there is a need for outside interaction.

Delegatecall

Paraphrased from the solidity docs:

“Delegatecall is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.

This means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.”

This low-level function has been very useful as it’s the backbone for implementing Libraries and modularizing code. However it opens up the doors to vulnerabilities as essentially your contract is allowing anyone to do whatever they want with their state.

In the example below, an attacker can call contract Delegate’s public function pwn and since the call is in the context of Delegation, they can claim ownership of the contract.

The Parity hack involved a combination of both insecure visibility modifiers and misuse of delegate call with abritrary data. The vulnerable contract’s function implemented delegatecall and a function from another contract that could modify ownership was left public. That allowed an attacker to craft the msg.data field to call the vulnerable function.

As for what would be included in the msg.data field, that is the signature of the function that you want to call. Signature here means the first 8 bytes of the sha3 (alias for keccak256)hash of the function prototype.

In this case:
web3.sha3("pwn()").slice(0, 10) --> 0xdd365b8b
If the function takes an argument, pwn(uint256 x):
web3.sha3("pwn(uint256)").slice(0,10) --> 0x35f4581b

3. Reentrancy (TheDAO hack)

Solidity’s call function when called with value forwards all the gas it received. In the snippet below, the call is made before actually reducing the sender’s balance. This opened up a vulnerability which is described very well in reddit comment when TheDAO hack happened:

“In simple words, it’s like the bank teller doesn’t change your balance until she has given you all the money you requested. “Can I withdraw $500? Wait, before that, can I withdraw $500?”

And so on. The smart contracts as designed only check you have $500 at the beinning, once, and allow themselves to be interrupted.”

As described in detail here, the fix is to reduce the sender’s balance before making the transfer of value. For people who have worked with parallel programming, another solution is using mutexes, mitigating all kinds of race conditions altogether.

Currently, using msg.sender.transfer(_value) is the best practice. If you really need to use send userequire(msg.sender.send(_value));

(Thank you Hayden Adams and Paulius for the fix on the above statement!)

This concludes Part 1. In the next article, we will discuss some lesser known exploits, the tools you should add to your workflow and the future of smart contract security.

Sign up below to get alerted when it goes live.

Loom Network is the multichain interop platform for scaling high-performance dapps — already live in production, audited, and battle-tested.

Deploy your dapp to Loom’s Basechain once and reach the widest possible user base across all major blockchains today.

New to Loom? Start here.

Want to stake your LOOM tokens and help secure Basechain? Find out how.

Like what we’re doing here? Stay in the loop by signing up for our private mailing list.

--

--