How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 1)
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.
Likewise, in the inverse case, when the number is unsigned, decrementing will underflow the number, resulting in the maximum possible value.
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
internal unless there is a need for outside interaction.
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.valuedo 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)
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.
msg.sender.transfer(_value) is the best practice. If you really need to use
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 blockchain platform of choice for serious dapp developers — the Universal Layer 2 that provides developers the tools they need to build functional user-facing dapps today.
New to Loom? Start here.
Want to stake your LOOM tokens and help secure Loom Network? Find out how.
And if you enjoyed this article and want to stay in the loop, go ahead and sign up for our private mailing list.