Smart Contract Security Vulnerabilities

A Case Study with Real Solidity Code

HeartBank®
HeartBank Studio
4 min readAug 28, 2018

--

Introduction

Anticipating that the smart contracts we create can potentially garner massive funding for our users, our security auditing process ensures that our contracts are not susceptible to common attacks at the very least. This article is an addendum to How to Decentralize the Film Industry to explain the security measures we took in our Solidity code.

For the design patterns we followed based on industry best practices, take a look at this related article:

Contract Source Code on Remix:

https://gist.github.com/thonly/dba764a0bc28544f479d81ba174e1511

BoxOffice.sol is our main contract, a factory that creates instances of BoxOfficeMovie.sol per film project. BoxOfficeMovie inherits the ERC20 standard contract from OpenZeppelin and uses its SafeMath library to securely perform arithmetic operations. The HeartBankCoin contract and the Oracle contract and the OracleLibrary and OracleStorage contracts it depends are ancillary while OracleRegistry is for demonstrating the Upgradable Registry pattern.

Integer Overflow and Underflow

Attackers can take advantage of the fact that integers can overflow and underflow in the EVM. To protect against this, we import the SafeMath library from OpenZeppelin to use in our arithmetic calculations, e.g.:

Forcibly Sending Ether to a Contract

Attackers can also take advantage of the subtlety that ether can be sent to a contract without ever triggering its callback function. This can be achieved by self-destructing a malicious contract and specifying the target contract as the recipient of the remaining ether. Attackers can also precompute a contract’s address ahead of time and send ether to it before the contract is even deployed! For these reasons, we design our contracts to not depend on the contract balance for logic. Instead, we use storage variables to keep track of the admin’s balance from collected fees and the filmmaker’s balance from ticket sales, irrespective of how much ether our contracts have received:

Timestamp Dependence

The now keyword is an alias for block.timestamp, which miners can manipulate to within a 30-second window. In our dApp, filmmakers can specify a salesEndDate to which buyers must adhere. Though our buyTickets() function depends on now to perform this check, it’s not a sensitive requirement as 30-second manipulation by miners can be tolerated.

Front Running

Closely related to timestamp dependence is Transaction-Ordering Dependence, aka Front Running. When transactions are broadcasted to the network, they first enter the mempool from which miners can choose which ones and in which order they want to include in their block (within a 30-second window). This can be problematic because they know in advance what transactions are about to occur. Fortunately for us, this is not problematic because our sales period requirement is not a critical check.

Reentrancy

Whenever <address>.transfer() is invoked, it provides an opportunity for control to flow to a corrupt contract, because address needs not always be an externally owned account. When called, the called contract’s fallback function can contain code to call back (reenter) the calling contract’s function in a recursive manner that can be detrimental. For our withdrawFund() function to work, we cannot avoid making an external call to the recipient’s address. Therefore, we utilize the withdrawal design pattern to separate the accounting logic from the transfer logic, making sure to finish all internal state changes first before invoking the external call. Usually, this means transfer() is the last step. Moreover, potential cross-function race conditions such as calling internal functions that call external functions are also designed away.

Finishing all internal work before making external calls also protects against denial of service attacks. The called contract’s fallback can always revert, which also halts the execution of the calling contract. If there are necessary operations further along, they will never get executed.

By utilizing the withdrawal design pattern to favor pull payments over push payments, we also avoid denial of service attacks that target the block gas limit. We could have looped through an array of addresses, pushing payments to the recipients automatically for their convenience. However, attackers could take advantage of the fact that we are looping through an array of unknown size, creating many corrupt recipient accounts to cause our function to hit the block gas limit whenever it executes. The effect, our contract becomes unusable.

Did we miss anything? Let us know!

Reference: https://consensys.github.io/smart-contract-best-practices/known_attacks

--

--