TokenHook: Secure ERC-20 smart contract

07/07/2021 ∙ by Reza Rahimian, et al. ∙ 0

ERC-20 is the most prominent Ethereum standard for fungible tokens. Tokens implementing the ERC-20 interface can interoperate with a large number of already deployed internet-based services and Ethereum-based smart contracts. In recent years, security vulnerabilities in ERC-20 have received special attention due to their widespread use and increased value. We systemize these vulnerabilities and their applicability to ERC-20 tokens, which has not been done before. Next, we use our domain expertise to provide a new implementation of the ERC-20 interface that is freely available in Vyper and Solidity, and has enhanced security properties and stronger compliance with best practices compared to the sole surviving reference implementation (from OpenZeppelin) in the ERC-20 specification. Finally, we use our implementation to study the effectiveness of seven static analysis tools, designed for general smart contracts, for identifying ERC-20 specific vulnerabilities. We find large inconsistencies across the tools and a high number of false positives which shows there is room for further improvement of these tools.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

The Ethereum blockchain [19, 79] allows users to build and deploy decentralized applications (DApps) that can accept and use its protocol-level cryptocurrency ETH. Many DApps also issue or use custom tokens. Such tokens could be financial products, in-house currencies, voting rights for DApp governance, or other valuable assets. To encourage interoperability with other DApps and web applications (exchanges, wallets, etc.), the Ethereum community accepted a popular token standard (specifically for fungible tokens) called ERC-20 [25]. While numerous ERC-20 extensions or replacements have been proposed, ERC-20 remains prominent. Of the 2.5M [56] smart contracts on the Ethereum network, 260K are tokens [70] and 98% of these tokens are ERC-20 [23].

The development of smart contracts has been proven to be error-prone, and as a result, smart contracts are often riddled with security vulnerabilities. An early study in 2016 found that 45% of smart contracts at that time had vulnerabilities [41]. ERC-20 tokens are subset of smart contracts and security is particularly important given that many tokens have considerable market capitalization (e.g., USDT, BNB, UNI, DAI, etc.). As tokens can be held by commercial firms, in addition to individuals, and firms need audited financial statements in certain circumstances, the correctness of the contract issuing the tokens is now in the purview of professional auditors. Later, we examine one static anaylsis tool from a ‘big-four’ auditing firm.

Contributions

Ethereum has undergone numerous security attacks that have collectively caused more than US$100M in financial losses [27, 49, 47, 60, 52, 2]. Although research has been done on smart contract vulnerabilities in the past [34], we focus specifically on ERC-20 tokens.

  1. We study all known vulnerabilities and cross-check their relevance to ERC-20 token contracts, systematizing a comprehensive set of 82 distinct vulnerabilities and best practices.

  2. While not strictly a research contribution, we believe that our newly acquired specialized domain knowledge should be put to use. Thus, we provide a new ERC-20 implementation, TokenHook

    , that is open source and freely available in both Vyper and Solidity.

  3. TokenHook is positioned to increase software diversity: currently, no Vyper ERC-20 implementation is considered a reference implementation, and only one Solidity implementation is actively maintained (OpenZeppelin’s [45]). Relative to this implementation, TokenHook has enhanced security properties and stronger compliance with best practices.

  4. Perhaps of independent interest, we report on differences between Vyper and Solidity when implementing the same contract.

  5. We use TokenHook as a benchmark implementation to explore the completeness and precision of seven auditing tools that are widely used in industry to detect security vulnerabilities. We conclude that while these tools are better than nothing, they do not replace the role of a security expert in developing and reviewing smart contract code.

2 Sample of high profile vulnerabilities

In this section, we examine general attack vectors and cross-check their applicability to ERC-20 tokens. We sample some high profile vulnerabilities, typically ones that have been exploited in real world ERC-20 tokens

[42, 34, 15, 13, 40]. For each, we (i) briefly explain technical details, (ii) the ability to affect ERC-20 tokens, and (iii) discuss mitigation techniques. Later we will compile a more comprehensive list of 82 vulnerabilities and best practices (see Table2), including these, however space will not permit us to discuss each one at the same level of detail as the ones we highlight in this section (however we will include a simple statement describing the issue and the mitigation).

2.1 Multiple withdrawal

This ERC-20-specific issue was originally raised in 2017 [75, 32]. It can be considered as a transaction-ordering [8] or front-running [18] attack. There are two ERC-20 functions (i.e., Approve() and transferFrom()) that can be used to authorize a third party for transferring tokens on behalf of someone else. Using these functions in an undesirable situation (i.e., front-running or race-condition) can result in allowing a malicious authorized entity to transfer more tokens than the owner wanted. There are several suggestions to extend ERC-20 standard (e.g., MonolithDAO [74] and its extension in OpenZeppelin [45]) by adding new functions (i.e., decreaseApproval() and increaseApproval()), however, securing transferFrom() method is the effective one while adhering specifications of the ERC-20 standard [53].

2.2 Arithmetic Over/Under Flows.

An integer overflow is a well known issue in many programming languages. For ERC-20, one notable exploit was in April 2018 that targeted the BEC Token [10] and resulted in some exchanges (e.g., OKEx, Poloniex, etc.) suspending deposits and withdrawals of all tokens. Although BEC developers had considered most of the security measurements, only line 261 was vulnerable [26, 49]. The attacker was able to pass a combination of input values to transfer large amount of tokens [54]. It was even larger than the initial supply of the token, allowing the attacker to take control of token financing and manipulate the price. In Solidity, integer overflows do not throw an exception at runtime. This is by design and can be prevented by using the SafeMath library [46] wherein a+b will be replaced by a.add(b) and throws an exception in the case of arithmetic overflow. Vyper has built-in support for this issue and no need to use SafeMath library.

2.3 Re-entrancy

One of the most studied vulnerabilities is re-entrancy, which resulted in a US$50M attack on a DApp (called the DAO) in 2016 and triggered an Ethereum hard-fork to revert [27]. At first glance, re-entrancy might seem inapplicable to ERC-20 however any function that changes internal state, such as balances, need to be checked. Further, some ERC-20 extensions could also be problematic. One example is ORBT tokens [55] which support token exchange with ETH without going through a crypto-exchange [61]: an attacker can call the exchange function to sell the token and get back equivalent in ETH. However, if the ETH is transferred in a vulnerable way before reaching the end of the function and updating the balances, control is transferred to the attacker receiving the funds and the same function could be invoked over and over again within the limits of a single transaction, draining excessive ETH from the token contract. This variant of the attack is known as same-function re-entrancy, but it has three other variants: cross-function, delegated and create-based  [58]. Mutex [77] and CEI [14] techniques can be used to prevent it. In Mutex, a state variable is used to lock/unlock transferred ETH by the lock owner (i.e., token contract). The lock variable fails subsequent calls until finishing the first call and changing requester balance. CEI updates the requester balance before transferring any fund. All interactions (i.e., external calls) happen at the end of the function and prevents recursive calls. Although CEI does not require a state variable and consumes less Gas, developers must be careful enough to update balances before external calls. Mutex is more efficient and blocks cross-function attack at the beginning of the function regardless of internal update sequences. CEI can also be considered as a best practice and basic mitigation for the same-function re-entrancy. We implement a sell() and buy() function in TokenHook for exchanging between tokens and ETH. sell() allows token holders to exchange tokens for ETH and buy() accepts ETH by adjusting buyer’s token balance. It is used to buy and sell tokens at a fixed price (e.g., an initial coin offering (ICO), prediction market portfolios [5]) independent of crypto-exchanges, which introduce a delay (for the token to be listed) and fees. Both CEI and Mutex are used in TokenHook to mitigate two variants of re-entrancy attack.

2.4 Unchecked return values

In Solidity, sending ETH to external addresses is supported by three options: call.value(), transfer(), or send(). The transfer() method reverts all changes if the external call fails, while the other two return a boolean value and manual check is required to revert transaction to the initial state [3]. Before the Istanbul hard-fork [1], transfer() was the preferred way of sending ETH. It mitigates reentry by ensuring ETH recipients would not have enough gas (i.e., a 2300 limit) to do anything meaningful beyond logging the transfer when execution control was passed to them. EIP-1884 [33] has increased the gas cost of some opcodes that causes issues with transfer()111After Istanbul, the fallback() function consumes more than 2300 Gas if called via transfer() or send() methods.. This has led to community advice to use call.value() and rely on one of the above re-entrancy mitigations (i.e., Mutex or CEI) [77, 16]. This issue is addresses in Vyper and there is no need to check return value of send() function.

2.5 Frozen Ether

As ERC-20 tokens can receive and hold ETH, just like a user accounts, functions need to be defined to withdraw deposited ETH (including unexpected ETH). If these functions are not defined correctly, an ERC-20 token might hold ETH with no way of recovering it (cf. Parity Wallet [48]). If necessary, developers can require multiple signatures to withdraw ETH.

2.6 Unprotected Ether Withdrawal

Improper access control may allow unauthorized persons to withdraw ETH from smart contracts (cf. Rubixi [59]). Therefore, withdrawals must be triggered by only authorized accounts and ideally multiple parties.

2.7 State variable manipulation

The DELEGATECALL opcode enables a DApp to invoke external functions of other DApps and execute them in the context of calling contract (i.e., the invoked function can modify the state variables of the caller). This makes it possible to deploy libraries once and reuse the code in different contracts. However, the ability to manipulate internal state variables by external functions has lead to incidents where the entire contract was hijacked (cf. the second hack of Parity MultiSig Wallet [2]). Preventive techniques is to use Library keyword in Solidity to force the code to be stateless, where data is passed as inputs to functions and passed back as outputs and no internal storage is permitted [21]. There are two types of Library: Embedded and Linked. Embedded libraries have only internal functions (EVM uses JUMP opcode instead of DELEGATECALL), in contrast to linked libraries that have public or external functions (EVM initiate a “message call”). Deployment of linked libraries generates a unique address on the blockchain while the code of embedded libraries will be added to the contract’s code  [35]. It is recommended to use Embedded libraries to mitigate this attack.

2.8 Balance manipulation

ERC-20 tokens generally receive ETH via a payable function [22] (i.e., receive(), fallback(), etc.), however, it is possible to send ETH without triggering payable functions, for example via selfdestruct() that is initiated by another contract [65]. This can cause an oversight where ERC-20 may not properly account for the amount of ETH they have received [68]. For example, A contract might use ETH balance to calculate exchange rate dynamically. Forcing ETH by attacker may affect calculations and get lower exchange rate. To fortify this vulnerability, contract logic should avoid using exact values of the contract balance and keep track of the known deposited ETH by a new state variable. Although we use address(this).balance in TokenHook, we do not check the exact value of it (i.e., address(this).balance == 0.5 ether)—we only check whether the contract has enough ETH to send out or not. Therefore, there is no need to use a new state variable and consume more Gas to track contract’s ETH. However, for developers who need to track it manually, we provide contractBalance variable. Two complementary functions are also considered to get current contract balance and check unexpected received ETH (i.e., getContractBalance() and unexpectedEther()).

2.9 Public visibility

In Solidity, visibility of functions are Public by default and they can be called by any external user/contract. In the Parity MultiSig Wallet hack [52], an attacker was able to call public functions and reset the ownership address of the contract, triggering a $31M USD theft. It is recommended to explicitly specify visibility of functions instead of default Public visibility.

3 A sample of best practices

We highlight a few best practices for developing DApps. Some best practices are specific to ERC-20, while others are generic for all DApps—in which case, we discuss their relevance to ERC-20.

3.1 Compliance with ERC-20.

According to the ERC-20 specifications, all six methods and two events must be implemented and are not optional. Tokens that do not implement all methods (e.g., GNT which does not implement the approve(), allowance() and transferFrom() functions due to front-running[30]) can cause failed function calls from other applications. They might also be vulnerable to complex attacks (e.g., Fake deposit vulnerability[36], Missing return value bug[9]).

3.2 External visibility.

Solidity supports two types of function calls: internal and external [20]. Note that functions calls are different than functions visibility (i.e., Public, Private, Internal and External) which confusingly uses overlapping terminology. Internal function calls expect arguments to be in memory and the EVM copies the arguments to memory. Internal calls use JUMP opcodes instead of creating an EVM call.222Also known as “message call” when a contract calls a function of another contract. Conversely, External function calls create an EVM call and can read arguments directly from the calldata space. This is cheaper than allocating new memory and designed as a read-only byte-addressable space where the data parameter of a transaction or call is held[67]. A best practice is to use external visibility when we expect that functions will be called externally.

3.3 Fail-Safe Mode.

In the case of a detected anomaly or attack on a deployed ERC-20 token, the functionality of the token can be frozen pending further investigation. For regulated tokens, the ability for a regulator to issue a ‘cease trade’ order is also generally required.

3.4 Firing events.

In ERC-20 standard, there are two defined events: Approval and Transfer. The first event logs successful allowance changes by token holders and the second logs successful token transfers by the transfer() and transferFrom(). These two events must be fired to notify external application on occurred changes. The external application (e.g., TokenScope[4]) might use them to detect inconsistent behaviors, update balances, show UI notifications, or to check new token approvals. It is a best practice to fire an event for every state variable change.

3.5 Global or Miner controlled variables.

Since malicious miners have the ability to manipulate global Solidity variables (e.g., block.timestamp, block.number, block.difficulty, etc.), it is recommended to avoid these variables in ERC-20 tokens.

3.6 Proxy contracts.

An ERC-20 token can be deployed with a pair of contracts: a proxy contract that passes through all the function calls to a second functioning ERC-20 contract[69, 44]. One use of proxy contract is when upgrades are required—a new functional contract can be deployed and the proxy is modified to point at the update. Form audit point of view, it is recommended to have non-upgradable ERC-20 tokens.

3.7 DoS with Unexpected revert.

A function that attempts to complete many operations that individually may revert could deadlock if one operation always fails. For example, transfer() can throw an exception—if one transfer in a sequence fails, the whole sequence fails. One standard practice is to account for ETH owed and require withdrawals through a dedicated function. In TokenHook, ETH is only transferred to a single party in a single function sell(). It seems overkill to implement a whole accounting system for this. As a consequence, a seller that is incapable of receiving ETH (e.g., operating from a contract that is not payable) will be unable to sell their tokens for ETH. However they can recover by transferring the tokens to a new address to sell from.

3.8 Unprotected SELFDESTRUCT

Another vulnerability stemming from the second Parity wallet attack [2] is protecting the SELFDESTRUCT opcode which removes a contract from Ethereum. The self-destruct method is used to kill the contract and its associated storage. ERC-20 tokens should not contain SELFDESTRUCT opcode unless there is a multi approval mechanism.

3.9 DoS with block gas limit.

The use of loops in contracts is not efficient and requires considerable amount of Gas to execute. It might also cause DoS attack since blocks has a Gas limit. If execution of a function exceeds the block gas limit, all transactions in that block will fail. Hence, it is recommended to not use loops and rely on mappings variables in ERC-20 tokens.

Figure 1: Architecture of the Ethereum blockchain in layers, including the interactive environment (i.e., application layer). ERC-20 tokens falls under the Smart Contracts category in Contract Layer.

4 TokenHook

TokenHook is our ERC20-compliant implementation written in Vyper (v. 0.2.8) and Solidity (v. 0.8.4) 333TokenHook deployed on Rinkeby at https://bit.ly/33wDENx (Solidity) and https://bit.ly/3dXaaPc (Vyper). Mainnet at https://bit.ly/35FMbAf (Solidity 0.5.11). It can be customized by developers, who can refer to each mitigation technique separately and address specific attacks. The presence of security vulnerability in supplementary layers (i.e., consensus, data, network. etc.) affect the entire Ethereum blockchain, not necessarily ERC-20 tokens. Therefore, vulnerabilities in other layers are assumed to be out of the scope. Required comments have been also added to clarify the usage of each function. Standard functionalities of the token (i.e., approve(), transfer(), transferFrom(), etc.) have been unit tested. A demonstration of token interactions and event triggering can also be seen on Etherscan.444Etherscan: https://bit.ly/33xHfL2, https://bit.ly/35TimMW and https://bit.ly/3eFAnAZ

Among the layers of the Ethereum blockchain, ERC-20 tokens fall under the Contract layer in which DApps are executed. The presence of a security vulnerability in supplementary layers affect the entire Ethereum blockchain, not necessarily ERC-20 tokens. Therefore, vulnerabilities in other layers are assumed to be out of the scope. (e.g., Indistinguishable chains at the data layer, the 51% attack at the consensus layer, Unlimited nodes creation at network layer, and Web3.js Arbitrary File Write at application layer).

Moreover, we exclude vulnerabilities identified in now outdated compiler versions. Examples: Constructor name ambiguity in versions before 0.4.22, Uninitialized storage pointer in versions before 0.5.0, Function default visibility in versions before 0.5.0, Typographical error in versions before 0.5.8, Deprecated solidity functions in versions before 0.4.25, Assert Violation in versions before 0.4.10, Under-priced DoS attack before EIP-150 & EIP-1884).

4.1 Security features

In our research, we developed 82 security vulnerabilities and best practices for ERC-20. We concentrate here on how TokenHook mitigates these attacks. While many of these attacks are no doubt very familiar to the reader, our emphasis is on their relevance to ERC-20.

4.1.1 Multiple Withdrawal Attack

Without our counter-measure, an attacker can use a front-running attack [8, 18] to transfer more tokens than what is intended (approved) by the token holder. We secure the transferFrom() function by tracking transferred tokens to mitigate the multiple withdrawal attack [53]. Securing the transferFrom() function is fully compliant with the ERC-20 standard without the need of introducing new functions such as decreaseApproval() and increaseApproval().

4.1.2 Arithmetic Over/Under Flows

In Solidity implementation, we use the SafeMath library in all arithmetic operations to catch over/under flows. Using it in Vyper is not required due to built-in checks.

4.1.3 Re-entrancy

At first glance, re-entrancy might seem inapplicable to ERC-20. However any function that changes internal state, such as balances, need to be checked. We use Checks-Effects-Interactions pattern (CEI) [16] in both Vyper and Solidity implementations to mitigate same-function re-entrancy attack. Mutual exclusion (Mutex) [77] is also used to address cross-function re-entrancy attack. Vyper supports Mutex by adding @nonreentrant(<key>) decorator on a function and we use noReentrancy modifier in Solidity to apply Mutex. Therefore, both re-entrancy variants are addressed in TokenHook.

4.1.4 Unchecked return values

Unlike built-in support in Vyper, we must check the return value of call.value() in Solidity to revert failed fund transfers. It mitigates the unchecked return values attack while making the token contract compatible with EIP-1884 [33].

4.1.5 Frozen Ether

We mitigate this issue by defining a withdraw() function that allows the owner to transfer all Ether out of the token contract. Otherwise, unexpected Ether forced onto the token contract (e.g., from another contract running selfdestruct) will be stuck forever.

4.1.6 Unprotected Ether Withdrawal

We enforce authentication before transferring any funds out of the contract to mitigate unprotected Ether withdrawal. Explicit check is added to the Vyper code and onlyOwner modifier is used in Solidity implementation. It allows only owner to call withdraw() function and protects unauthorized Ether withdrawals.

4.1.7 State variable manipulation

In the Solidity implementation, we use embedded Library code (for SafeMath) to avoid external calls and mitigate the state variable manipulation attack. It also reduces gas costs since calling functions in embedded libraries requires less gas than external calls.

4.1.8 Function visibility

We carefully define the visibility of each function. Most of the functions are declared as External (e.g., Approve(), Transfer(), etc.) per specifications of ERC-20 standard.

4.2 Best practices and enhancements

We also take into account a number of best practices that have been accepted by the Ethereum community to proactively prevent known vulnerabilities [12]. Again, we highlight several of these while placing the background details in the appendix.

4.2.1 Compliance with ERC-20

We implement all ERC-20 functions to make it fully compatible with the standard. Compliance is important for ensuring that other DApps and web apps (i.e., crypto-wallets, crypto-exchanges, web services, etc.) compose with TokenHook as expected.

4.2.2 External visibility

To improve performance, we apply an external visibility (instead of public visibility in the standard) for interactive functions (e.g., approve() and transfer(), etc.). External functions can read arguments directly from non-persistent calldata instead of allocating persistent memory by the EVM.

4.2.3 Fail-Safe Mode

We implement a ‘cease trade’ operation that will freeze the token in the case of new security threats or new legal requirements (e.g., Liberty Reserve  [78] or TON cryptocurrency [17]). To freeze all functionality of TokenHook, the owner (or multiple parties) can call the function pause() which sets a lock variable. All critical methods are either marked with a notPaused modifier (in Solidity) or explicit check (in Vyper), that will throw exceptions until functionality is restored using unpause().

4.2.4 Firing events

We define nine extra events: Buy, Sell, Received, Withdrawal, Pause, Change, ChangeOwner, Mint and Burn. The name of each event indicates its function except Change event which logs any state variable updates. It can be used to watch for token inconsistent behavior (e.g., via TokenScope [4]) and react accordingly.

4.2.5 Proxy contracts

We choose to make TokenHook non-upgradable so it can be audited, and upgrades will not introduce new vulnerabilities that did not exist at the time of the initial audit.

4.2.6 Other enhancements

We also follow other best practices such as not using batch processing in sell() function to avoid DoS with unexpected revert issue, not using miner controlled variable in conditional statements, and not using SELFDESTRUCT.

4.3 Implementing in Vyper vs. Solidity

Although Vyper offers less features than Solidity (e.g., no class inheritance, modifiers, inline assembly, function/operator overloading, etc. [20]), the Vyper compiler includes built-in security checks. Table 1 provides a comparison between the two from the perspective of TokenHook (see [39] for a broader comparison on vulnerabilities). Security and performance are advantages of Vyper. However, Vyper may not be a preferred option for production (“Vyper is beta software, use with care” [76]), most of the auditing tools only support Solidity,555Vyper support is recently added to some tools (e.g., Crytic-compile, Manticore and Echidna). Slither integration is still in progress [43] and Solidity currently enjoys widespread implementation, developer tools, and developer experience.

Vulnerability (Vul.) or
Best Practice (BP.)
TokenHook
Implementation
Comment
Vyper Solidity
Arithmetic Over/Under Flows Vul. +
- Vyper includes built-in checks for over/under flows.
- SafeMath library is required in Solidity to mitigate the attack.
Re-Entrancy Vul. +
- @nonreentrant decorator places a lock on functions to mitigate the attack.
- noReentrancy modifier is required in Solidity.
Unchecked return values Vul. +
- It is already addressed in Vyper.
- There is a need in Solidity to check return values explicitly.
Code readability BP. +
- No inheritance in Vyper enforces simpler design.
- Solidity allows inline assemblies which is riskier and decreases readability.
Contract complexity BP. + - 300 lines in Vyper have the same functionality as the Solidity with 500 lines.
Auditable BP. + - Most of the auditing tools are able to analyze Solidity contracts.
Compatibility BP. +
- Majority of the current Ethereum projects are based on Solidity.
- Developers are more familiar with Solidity than Vyper.
Production readiness BP. +
- Vyper is not as mature as Solidity in terms of stability, documentation, etc.
- Solidity is adapted by a larger development community.
Table 1: Comparison of TokenHook implementation in Vyper and Solidity. The plus sign can be considered as an advantage. However, both versions of TokenHook offer the same level of security.

4.4 Need for another reference implementation

The authors of the ERC-20 standard reference two sample Solidity implementations: one that is actively maintained by OpenZeppelin [45] and one that has been deprecated by ConsenSys [7] (and now refers to the OpenZeppelin implementation). As expected, the OpenZeppelin template is very popular within the Solidity developers [57, 80, 51].

OpenZeppelin’s implementation is actually part of a small portfolio of implementations (ERC20, ERC721, ERC777, and ERC1155). Code reuse across the four implementations adds complexity for a developer that only wants ERC-20. This might be the reason for not supporting Vyper in OpenZeppelin’s implementation. No inheritance in Vyper requires different implementation than the current object-oriented OpenZeppelin contracts. Further, most audit tools are not able to import libraries/interfaces from external files (e.g., SafeMath.sol, IERC20.sol). By contrast, TokenHook uses a flat layout in a single file that is specific to ERC-20. It does not use inheritance in Solidity which allows similar implementation in Vyper.

TokenHook makes other improvements over the OpenZeppelin implementation. For example, OpenZeppelin introduces two new functions to mitigate the multiple withdraw attack: increaseAllowance() and decreaseAllowance(). However these are not part of the ERC-20 standard and are not interoperable with other applications that expect to use approve() and transferFrom(). TokenHook secures transferFrom() to prevent the attack (following [53]) and is interoperable with legacy DApps and web apps. Additionally, TokenHook mitigates the frozen Ether issue by introducing a withdraw() function, while ETH forced into the OpenZeppelin implementation is forever unrecoverable. Both contracts implement a fail-safe mode, however this logic is internal to TokenHook, while OpenZeppelin requires an external Pausable.sol contract.

Diversity in software is important for robustness and security [28, 29]. For ERC-20, a variety of implementations will reduce the impact of a single bug in a single implementation. For example, between 17 March 2017 and 13 July 2017, OpenZeppelin’s implementation used the wrong interface and affected 130 tokens [9]. TokenHook increases the diversity of ERC-20 Solidity implementations and addresses the lack of a reference implementation in Vyper.

max height=10cm \̇hfil angle=45,lap=0pt-0.5emEY Token Review angle=45,lap=0pt-0.5emSmart Check angle=45,lap=0pt-0.5emSecurify angle=45,lap=0pt-0.5emMythX (Mythril) angle=45,lap=0pt-0.5emContract Guard angle=45,lap=0pt-0.5emSlither angle=45,lap=0pt-0.5emOdin Vulnerability or best practice ID SWC Mitigation or recommendation Security tools 1 100 Function default visibility Specifying function visibility, external, public, internal or private 2 101 Integer Overflow and Underflow Utilizing the SafeMath library to mitigate over/under value assignments 3 102 Outdated Compiler Version Using proper Solidity version to protect against compiler attacks 4 103 Floating Pragma Locking the pragma to avoid deployments using outdated compiler version 5 104 Unchecked Call Return Value Checking call() return value to prevent unexpected behavior in DApps 6 105 Unprotected Ether Withdrawal Authorizing only trusted parties to trigger ETH withdrawals 7 106 Unprotected SELFDESTRUCT Instruction Removing self-destruct functionality or approving it by multiple parties 8 107 Re-entrancy Using CEI and Mutex to mitigate self-function and cross-function attack 9 108 State variable default visibility Specifying visibility of all variables, public, private or internal 10 109 Uninitialized Storage Pointer Initializing variables upon declaration to prevent unexpected storage access 11 110 Assert Violation Using require() statement to validate inputs, checking efficiency of the code 12 111 Use of Deprecated Solidity Functions Using new alternatives functions such as keccak256() instead of sha3() 13 112 Delegatecall to untrusted callee Calling into trusted contracts to avoid storage access by malicious contracts 14 113 DoS with Failed Call Avoid multiple external calls where one error may fail other transactions 15 114 Transaction Order Dependence Preventing race conditions by securing approve() or transferFrom() 16 115 Authorization through tx.origin Using msg.sender to authorize transaction initiator instead of originator 17 116 Block values as a proxy for time Not using block.timestamp or block.number to perform functionalities 18 117 Signature Malleability Not using signed message hash to avoid signatures alteration 19 118 Incorrect Constructor Name Using constructor keyword which does not match with contract name 20 119 Shadowing State Variables Removing any variable ambiguities when inheriting other contracts 21 120 Weak Sources of Randomness from Chain Attributes Using oracles as source of randomness instead of block.timestamp 22 121 Missing Protection against Signature Replay Attacks Storing every message hash to perform signature verification 23 122 Lack of Proper Signature Verification Using alternate verification schemes if allowing off-chain signing 24 123 Requirement Violation Checking the code for allowing only valid external inputs 25 124 Write to Arbitrary Storage Location Controlling write to storage to prevent storage corruption by attackers 26 125 Incorrect Inheritance Order Inheriting from more general to specific when there are identical functions 27 126 Insufficient Gas Griefing Allowing trusted forwarders to relay transactions

Table 2: Auditing results of 7 smart contract analysis tools on TokenHook. =Passed audit, =False positive, =Failed audit, Empty=Not supported audit by the tool, =Informational, =Tool specific audit (No SWC registry), BP=Best practice

max height=10cm \̇hfil angle=45,lap=0pt-0.5emEY Token Review angle=45,lap=0pt-0.5emSmart Check angle=45,lap=0pt-0.5emSecurify angle=45,lap=0pt-0.5emMythX (Mythril) angle=45,lap=0pt-0.5emContract Guard angle=45,lap=0pt-0.5emSlither angle=45,lap=0pt-0.5emOdin Vulnerability or best practice ID SWC Mitigation or recommendation Security tools 28 127 Arbitrary Jump with Function Type Variable Minimizing use of assembly in the code 29 128 DoS With Block Gas Limit Avoiding loops across the code that may consume considerable resources 30 129 Typographical Error Using SafeMath library or performing checks on any math operation 31 130 Right-To-Left-Override control character (U+202E) Avoiding U+202E character which forces RTL text rendering 32 131 Presence of unused variables Removing all unused variables to decrease gas consumption 33 132 Unexpected Ether balance Avoiding Ether balance check in the code (e.g., this.balance == 0.24 Ether) 34 133 Hash Collisions With Variable Length Arguments Using abi.encode() instead of abi.encodePacked() to prevent hash collision 35 134 Message call with hardcoded gas amount Using .call.value()(””) which is compatible with EIP1884 36 135 Code With No Effects Writing unit tests to ensure producing the intended effects by DApps 37 136 Unencrypted Private Data On-Chain Storing un-encrypted private data off-chain 38 Allowance decreases upon transfer Decreasing allowance in transferFrom() method 39 Allowance function returns an accurate value Returning only value from the mapping instead of internal function logic 40 It is possible to cancel an existing allowance Possibility of setting allowance to 0 to revoke previous allowances 41 A transfer with an insufficient amount is reverted Checking balances in transfer() method before updating balances 42 Upon sending funds, the sender’s balance is updated Updating balances in transfer() or transferFrom() methods 43 The Transfer event correctly logged Emitting Transfer event in transfer() or transferFrom() functions 44 Transfer an amount that is greater than the allowance Checking balances in transferFrom() method before updating balances 45 Risk of short address attack is minimized Using recent Solidity version to mitigate the attack 46 Function names are unique No function overloading to avoid unexpected behavior 47 Using miner controlled variables Avoiding block.number, block.timestamp, block.difficulty, now, etc 48 Use of return in constructor Not using return in contract’s constructor 49 Throwing exceptions in transfer() and transferFrom() Returning true after successful execution or raising exception in failures 50 State variables that could be declared constant Adding constant attribute to variables like name, symbol, decimals, etc 51 Tautology or contradiction Fixing comparison in the code that are always true or false 52 Divide before multiply Ordering multiplication prior division to avoid integer truncation 53 Unchecked Send Ensuring that the return value of send() is always checked 54 BP Too many digits Using scientific notation to make the code readable and simpler to debug

Table 3: Continuation of Table2.

max height=10cm \̇hfil angle=45,lap=0pt-0.5emEY Token Review angle=45,lap=0pt-0.5emSmart Check angle=45,lap=0pt-0.5emSecurify angle=45,lap=0pt-0.5emMythX (Mythril) angle=45,lap=0pt-0.5emContract Guard angle=45,lap=0pt-0.5emSlither angle=45,lap=0pt-0.5emOdin Vulnerability or best practice ID SWC Mitigation or recommendation Security tools 55 BP The decreaseAllowance definition follows the standard Defining decreaseAllowance input and output variables as standard 56 BP The increaseAllowance definition follows the standard Defining increaseAllowance input and output variables as standard 57 BP Minimize attack surface Checking whether all the external functions are necessary or not 58 BP Transfer to the burn address is reverted Reverting transfer to 0x0 due to risk of total supply reduction 59 BP Source code is decentralized Not using hard-coded addresses in the code 60 BP Funds can be held only by user-controlled wallets Transferring tokens to users to avoid creating a secondary market 61 BP Code logic is simple to understand Avoiding code nesting which makes the code less intuitive 62 BP All functions are documented Using NatSpec format to explain expected behavior of functions 63 BP The Approval event is correctly logged Emitting Approval event in the approve() method 64 BP Acceptable gas cost of the approve() function Checking for maximum 50000 gas cost when executing the approve() 65 BP Acceptable gas cost of the transfer() function Checking for maximum 60000 gas cost when executing the transfer() 66 BP Emitting event when state changes Emitting Change event when changing state variable values 67 BP Use of unindexed arguments Using indexed arguments to facilitate external tools log searching 68 BP ERC-20 compliance Implementing all 6 functions and 2 events as specified in EIP-20 69 BP Conformance to naming conventions Following the Solidity naming convention to avoid confusion 70 BP Token decimal Declaring token decimal for external apps when displaying balances 71 BP Locked money (Freezing ETH) Implementing withdraw/reject functions to avoid ETH lost 72 BP Malicious libraries Not using modifiable third-party libraries 73 BP Payable fallback function Adding either fallback() or receive() function to receive ETH 74 BP Prefer external to public visibility level Improving the performance by replacing public with external 75 BP Token name Adding a token name variable for external apps 76 BP Error information in revert condition Adding error description in require()/revert() to clarify the reason 77 BP Complex Fallback Logging operations in the fallback() to avoid complex operations 78 BP Function Order Following fallback, external, public, internal and private order 79 BP Visibility Modifier Order Specifying visibility first and before modifiers in functions 80 BP Non-initialized return value Not specifying return for functions without output 6 81 BP Token symbol Adding token symbol variable for usage of external apps 82 BP Allowance spending is possible Ability of token transfer by transferFrom() to transfer tokens on behalf of another usercalc 99.5% success rate in performed audits by considering ’False Positives’ and ’Informational’ checks as ’Passed’ (More details in section5) 100% 100% 100% 100% 100% 100% 97%

Table 4: Continuation of Table3.

5 Auditing Tools and ERC-20

Finally, we conducted an experiment on code auditing tools using the Solidity implementation of TokenHook to understand the current state of automated volunerabiliy testing. Our results illuminate the (in)completeness and error-rate of such tools on one specific use-case (related work studies, in greater width and less depth, a variety of use-cases [11]). We did not adapt older tools that support significantly lower versions of the Solidity compiler (e.g., Oyente). We concentrated on Solidity as Vyper analysis is currently a paid services or penciled in for future support (e.g., Slither). The provided version number is based on the GitHub repository; tools without a version are web-based and were used in 2020:

  1. EY Smart Contract & Token Review by Ernst & Young Global Limited [24].

  2. SmartCheck by SmartDec [64].

  3. Securify v2.0 by ChainSecurity [72, 71].

  4. ContractGuard by GuardStrike [31].

  5. MythX by ConsenSys [6].

  6. Slither Analyzer v0.6.12 by Crytic [38].

  7. Odin by Sooho [66].

5.1 Analysis of audit results

A total of 82 audits have been conducted by these auditing tools that are summarized in Tables 2, 3 and 4. Audits include best practices and security vulnerabilities. To compile the list of 82, we referenced the knowledge-base of each tool [72, 64, 6, 31, 38], understood each threat, manually mapped the audit to the corresponding SWC registry [63], and manually determined when different tools were testing for the same vulnerability or best practice (which was not always clear from the tools’ own descriptions). Since each tool employs different methodology to analyze smart contracts (e.g., comparing with violation patterns, applying a set of rules, using static analysis, etc.), there are false positives to manually check. Many false positives are not simply due to old/unmaintained rules but actually require tool improvement. We provide some examples in this section.

MythX detects Re-entrancy attack in the noReentrancy modifier. In Solidity, modifiers are not like functions. They are used to add features or apply some restriction on functions [62]. Using modifiers is a known technique to implement Mutex and mitigate re-entrancy attack [73]. This is a false positive and note that other tools have not identified the attack in modifiers.

ContractGuard flags Re-entrancy attack in transfer() function while countermeasures (based on both CEI and Mutex 2.3) are implemented.

Slither detects two low level call vulnerabilities [37]. This is due to use of call.value() that is recommend way of transferring ETH after Istanbul hard-fork (EIP-1884). Therefore, adapting analyzers to new standards can improve accuracy of the security checks.

SmartCheck recommends not using SafeMath and check explicitly where overflows might be occurred. We consider this failed audit as false possible whereas utilizing SafeMath is a known technique to mitigate over/under flows. It also flags using a private modifier as a vulnerability by mentioning, “miners have access to all contracts’ data and developers must account for the lack of privacy in Ethereum”. However private visibility in Solidity concerns object-oriented inheritance not confidentiality. For actual confidentiality, the best practice is to encrypt private data or store them off-chain. The tool also warns against approve() in ERC-20 due to front-running attacks. Despite EIP-1884, it still recommends using of transfer() method with stipend of 2300 gas. There are other false positives such as SWC-105 and SWC-112 that are passed by other tools.

Securify detects the Re-entrancy attack due to unrestricted writes in the noReentrancy modifier [71]. Modifiers are the recommended approach and are not accessible by users. It also flags Delegatecall to Untrusted Callee (SWC-112) while there is no usage of delegatecall() in the code. It might be due to use of SafeMath library which is an embedded library. In Solidity, embedded libraries are called by JUMP commands instead of delegatecall(). Therefore, excluding embedded libraries from this check might improve accuracy of the tool. Similar to SmartCheck, it still recommends to use the transfer() method instead of call.value().

EY token review considers decreaseAllowance and increaseAllowance as standard ERC-20 functions and if not implemented, recognizes the code as vulnerable to a front-running. These two functions are not defined in the ERC-20 standard [25] and considered only by this tool as mandatory functions. There are other methods to prevent the attack while adhering ERC-20 specifications (see Rahimian et al. for a full paper on this attack and the basis of the mitigation in TokenHook [53]). The tool also falsely detects the Overflow, mitigated through SafeMath. Another identified issue is Funds can be held only by user-controlled wallets. The tool warns against any token transfer to Ethereum addresses that belong to smart contracts. However, interacting with ERC-20 token by other smart contracts was one of the main motivations of the standard. It also checks for maximum 50000 gas in approve() and 60000 in transfer() method. We could not find corresponding SWC registry or standard recommendation on these limitations and therefore consider them as informational.

Odin raises Outdated compiler version issue due to locking solidity version to 0.5.11. We have used this version due to its compatibility with other auditing tools.

Auditing Tool
ERC-20 Token
EY Token
Review
Smart
Check
Securify
MythX
(Mythril)
Contract
Guard
Slither Odin
Total
issues
TokenHook 9 11 4 2 10 2 2 40
TUSD 20 11 2 1 14 16 6 70
PAX 16 9 6 4 16 13 9 73
USDC 17 9 6 5 18 15 10 80
INO 11 10 14 8 14 24 12 93
HEDG 10 28 11 1 29 24 16 119
BNB 13 21 12 13 41 39 3 142
MKR 11 27 38 9 16 34 18 153
LINK 12 27 38 9 16 34 18 181
USDT 12 29 8 17 46 55 30 197
LEO 32 25 8 23 70 75 19 252
Table 5: Security flaws detected by seven auditing tools in TokenHook (the proposal) compared to top 10 ERC-20 tokens by market capitalization in May 2020. TokenHook has the lowest reported security issues (occurrences).

5.2 Comparing audits

After manually overriding the false positives, the average percentage of passed checks for TokenHook reaches to 99.5%. To pass the one missing check and reach a 100% success rate across all tools, we prepared the same code in Solidity version 0.8.4, however it cannot be audited anymore with most of the tools.

We repeated the same auditing process on the top ten tokens based on their market cap [23]. The result of all these evaluation have been summarized in Table 5 by considering false positives as failed audits. This provides the same evaluation conditions across all tokens. Since each tool uses different analysis methods, number of occurrences are considered for comparisons. For example, MythX detects two re-entrancy in TokenHook; therefore, two occurrences are counted instead of one.

As it can be seen in Table 5, TokenHook has the least number of security flaws (occurrences) compared to other tokens. We stress that detected security issues for TokenHook are all false positives. We are also up-front that this metric is not a perfect indication of security. The other tokens may also have many/all false positives (such an analysis would be interesting future work), and not all true positives can be exploited [50]. Mainly, we want to show this measurement as being consistent with our claims around the security of TokenHook. Had TokenHook, for example, had the highest number of occurrences, it would be a major red flag.

6 Conclusion

98% of tokens on Ethereum today implement ERC-20. While attention has been paid to the security of Ethereum DApps, threats to tokens can be specific to ERC-20 functionality. In this paper, we provide a detailed study of ERC-20 security, collecting and deduplicating applicable vulnerabilities and best practices, examining the ability of seven audit tools. Most importantly, we provide a concrete implementation of ERC-20 called TokenHook 666Compatible Solidity version of TokenHook (v. 0.5.11) deployed on Mainnet at https://bit.ly/35FMbAf and the latest Solidity (v. 0.8.4) on Rinkeby https://bit.ly/3tI139S. Vyper code at https://bit.ly/3dXaaPc.. It is designed to be secure against known vulnerabilities, and can serve as a second reference implementation to provide software diversity. We test it at Solidity version 0.5.11 (due to the limitation of the audit tools) and also provide it at version 0.8.4. Vyper implementation is also provided at version 0.2.8 to make ERC-20 contracts more secure and easier to audit. TokenHook can be used as template to deploy new ERC-20 tokens (e.g., ICOs, DApps, etc), migrate current vulnerable deployments, and to benchmark the precision of Ethereum audit tools.

References