CoinEx Research: An introduction to common vulnerabilities and attacks in smart contracts
What Is a Smart Contract?
Ethereum has two common types of accounts: Externally Owned Accounts (EOA) and Smart Contract Accounts (SCA).
EOA are very similar to the electronic financial accounts we commonly use to store funds and interact with applications. For instance, users deposit fiat currency through PayPal and interact with various websites, stores, and apps for payments. DeFi miners usually store cryptos in their EOA, interact with DeFi dApps, and deposit funds into dApps for profits. Yet EOA have a feature that electronic financial accounts do not possess: users must have their control over EOA verified through ownership of private keys— not your keys, not your coins.
SCA are also an type of account that is essentially associated with a segment of executable bytecode (also known as a smart contract). The smart contract describes various business logic and serves as the backend for dApps. However, despite having more restrictions compared to traditional Turing complete development languages, quasi-Turing complete smart contracts have still been vulnerable to numerous attacks, dealing countless blows to the blockchain industry.
Common Smart Contract Attacks
1. Reentrancy Attack
The most common and notorious attack is the reentrancy attack, which was responsible for the Ethereum fork that led to the creation of Ethereum Classic. In 2016, hackers executed a reentrancy attack on The DAO contract, stealing 3,600,000 ETH valued at over $150 million at the time. This attack, occurring during Ethereum’s early stages, devastated the ecosystem and shattered investor confidence, ultimately leading to a fork.
Here’s an example to help you better understand the principle of the reentrancy attack. Bank B previously lent some money to Bank A. One day, Bank B initiates a transfer to Bank A, requesting the transfer of all the money back to Bank B. The normal path is as follows:
Step 1: Bank B requests fund withdrawal
Step 2: Bank A transfers the funds to Bank B
Step 3: Bank A confirms the successful transfer to Bank B
Step 4: Bank A updates Bank B’s account balance.
However, if Bank B creates a loophole after Step 2 and continues to request all the money from Bank A without confirmation in Step 3, then the account balance of Bank A at Bank B will remain unchanged. This recursive call will empty all of Bank A’s assets.
Related Smart Contracts
Bank A’s contract includes two functions:
- deposit(): A deposit function that deposits money into Bank A and updates the user’s balance;
- withdraw(): A withdrawal function that allows users to withdraw all their funds from Bank A.
The attack contract of Bank B mainly involves a loop that triggers the receive() callback function, which in turn calls the withdraw() function of the Bank contract to drain the assets of Bank A through a sequence of 1 deposit, 1 withdrawal, and receive() callback function calls, and finally updates B’s balance in A. It includes two functions:
- receive(): A callback function triggered when ETH is received, which recursively calls the withdraw() function of the Bank contract to make withdrawals.
- attack(): It first calls the deposit() function of the Bank contract to refresh the balance and then the withdraw() function to initiate the first withdrawal, and triggers the receive() callback function to recursively call withdraw() to drain the assets of the Bank contract.
Implementing a reentrancy lock
A reentrancy lock is a modifier used to prevent reentrancy, ensuring that a call must complete its execution before it can be invoked again. For example, since the attack by Bank B requires calling the withdraw() function of the Bank contract for multiple times, it will fail with the implementation of a reentrancy lock.
How to Use It
2. Misuse of tx.origin
The main function of tx.origin in a smart contract is to retrieve the original account that initiated the transaction. Here, we will discuss two common variables in smart contracts: msg.sender and tx.origin. msg.sender retrieves the account directly calling the smart contract, while in the blockchain world, due to the nested and mutual calls of different smart contracts (such as DeFi Lego), tx.origin is needed to obtain the original account that initiated the transaction. A vulnerability arises when dApp developers only verify the security of tx.origin in the code, neglecting the security verification of attackers deploying intermediate contracts to bypass tx.origin and launch attacks.
Here’s an example to get you deep into the common attack scenario. Bill has a smart wallet that verifies whether Bill is the initiator of a transfer. Once, Bill minted an NFT on a phishing website. That allowed the website to obtain Bill’s identity and initiate a transfer from his smart wallet using his identity, resulting in asset losses. Under normal circumstances, users are less likely to fall for this trap, but when interacting with dApps using a wallet, they often forget to check the interaction prompts. For example, if both involve the Mint() function, careless users may easily fall into a phishing trap. The business logic within the phishing website is riddled with traps, so it’s important to check interaction prompts for errors during regular interactions.
Smart Wallet Contract
The smart wallet contract includes one function:
- transfer(): A withdrawal function that can only be initiated by the wallet owner, who in this case is Bill.
Phishing Attack Contract
In a phishing attack contract, Mint() induces users to transfer funds to a hacker’s address. It includes one function:
- Mint(): Once called, the phishing function internally executes transfer() of the Wallet contract. Since the original initiator is the user (in this example, Bill) himself, the verification require(tx.origin == owner, “Not owner”); will not be a problem. However, the target address for the transfer has already been tampered with to the hacker’s address, resulting in fund theft.
- Use msg.sender instead of tx.origin
No matter how many contract calls involved (Contract A → Contract B →…→ target contract), only verify msg.sender, i.e., the direct caller, to avoid attacks caused by malicious intermediate contracts.
- Verify tx.origin == msg.sender
This method can keep malicious contracts away, but developers need to consider their own business realities as it effectively isolates all other external contract calls.
3. Random Number Generator (RNG) Attack
This goes back to the gambling or betting dApp trend around 2018 and 2019. Typically, developers use certain seeds in smart contracts to generate random numbers to select winners during draws. Common seeds include block.number, block.timestamp, blockhash, and keccak256. However, miners can fully control these seeds, so in some cases, malicious miners may manipulate the variables to reap benefits.
Common Dice Contracts
The Dice contract includes one function:
- Bet(): A betting function where users input a betting number and pay an ETH. A random number is generated with multiple seeds, and if the betting number matches the random number, the user wins the entire prize pool.
Miner’s Attack Contract
Miners can win as long as they precompute the winning random number and execute it in the same block. This includes one function:
- attack(): A betting attack function, where the miner precomputes the winning random number. Since it is executed in the same block, blockhash(block.number – 1) and block.timestamp in the same block are the same. Then the miner calls Bet() of the Dice contract to complete the attack.
Use off-chain random numbers provided by oracle projects
Through services provided by oracle projects such as Chainlink, on-chain random numbers are injected into on-chain contracts to ensure randomness and security. However, oracle projects also carry centralization risks, thus necessitating more mature oracle services.
4. Replay Attack
A replay attack involves reinitiating a transaction using a previously used signature to steal funds. One of the best-known replay attacks in recent years was the theft of 20 million $OP tokens from the market maker Wintermute on Optimism, which was a cross-chain replay attack. Since Wintermute’s multi-signature wallet account was temporarily deployed on the Ethereum mainnet only, the hacker used the signature of the transaction for Wintermute’s deployment of a multi-signature address on Ethereum to re-execute the same transaction on the Optimism chain, thereby gaining control of the multi-signature wallet account on Optimism. A multi-signature wallet account is essentially a smart contract account, which also demonstrates a significant difference between SCA and EOA. For an EOA, a normal user only needs one private key to control all addresses on Ethereum and EVM-compatible chains (the address strings are exactly the same), while an SCA is effective on only one chain after being deployed.
Here, we provide an example of a typical replay attack (same-chain replay attack). Bill has a smart wallet that requires him to enter his electronic signature before each transaction can be executed. Now that the hacker Lucy has stolen Bill’s electronic signature , she can initiate an unlimited number of transactions to drain Bill’s smart wallet.
A contract with vulnerabilities consists of three functions:
- checkSig(): ECDSA verification function, ensuring that the verification result is the originally set signer.
- getMsgHash(): Function for generating hash, which combines to and amount to form hash.
- transfer(): Transfer function, allowing users to withdraw funds from the liquidity pool. Due to the lack of restrictions on the signature, the same signature can be reused, allowing hackers to continuously steal funds.
Include nonce in the signature combination to prevent replay attacks. The principle of the parameter is as below:
- nonce: It describes the variable of the number of transactions of an EOA in the blockchain network. It has order and uniqueness. With each additional transaction, the nonce value will increase by 1. The blockchain network will check whether the nonce of the transaction is consistent with the current nonce of the account. Therefore, a hacker would fail if he uses a used signature because the nonce value in the signature combination is less than the current nonce value of the EOA.
5. Denial of Service (DoS) Attack
The Denial of Service (DoS) attack is nothing new in the traditional Web2 world. It refers to any interference with a server, such as sending a large amount of junk or disruptive information, hampering or completely destroying availability. Similarly, smart contracts are plagued by such attacks, which essentially aim to make the smart contract malfunction.
Let’s see an example. Project A is conducting a public offering for the protocol token, where all users can contribute funds to the liquidity pool (Smart Contract) to purchase quotas on a first-come, first-served basis, and the excess funds will be returned to the participants. Hacker Alice exploits the attack contract to participate in the public offering. Once the liquidity pool attempts to return funds to Alice’s attack contract, a DoS attack will be triggered, preventing the return action from ever being realized. As a result, a large amount of funds are locked in the smart contract.
The public offering contract includes two functions:
- deposit(): deposit function, recording the address of the depositor and the amount contributed.
- refund(): refund function, with which the project team returns funds to the investors.
DoS Attack Contract
The DoS attack contract includes one function:
- attack(): Despite being an attack function, it does not have any issues. The main problem lies in the receive() payment callback function built into the Hacker contract, which includes a judgment of exceptions. Any external contract transferring funds to the Hacker contract will trigger an exception through revert(), thereby preventing the operation from completing.
- Avoid critical functionality getting stuck when invoking external contracts
Remove require(success, “Refund Fail!”); from the above refund() function of PublicSale contract, ensuring that the refund operation can continue even if a refund to a single address fails.
In the above refund() function of PublicSale contract, allow users to claim refunds on their own rather than distributing the refunds, thereby minimizing unnecessary interactions with external contracts.
6. permit Attack
In a permit attack, Account A provides the signature for a designated party in advance, and then Account B, upon obtaining the signature, can carry out authorized token transfers to steal a certain amount of tokens. Here, we primarily discuss two common functions for token authorization in Smart Contracts: approve() and permit().
In the common ERC20 contract, Account A can call approve() to authorize a certain amount of tokens for Account B, enabling the latter to transfer those tokens from the former. Additionally, permit() was introduced into ERC20 contracts in EIP-2612, and Uniswap has released a new token authorization standard, Permit2, in November 2022.
Here is an example. One day, Bill was browsing a blockchain news website when suddenly a Metamask signature popup appeared. Since many blockchain websites or applications use signatures to verify user logins, Bill didn’t think much of it and completed the signature directly. Five minutes later, his Metamask assets were drained. Bill then discovered in the blockchain explorer that an unknown address initiated a permit() transaction, followed by a transferFrom() transaction that emptied his wallet.
The two functions are as below:
- approve(): A standard authorization function where Account A authorizes a certain amount of funds to Account B.
- permit(): A signature authorization function where Account B submits and completes signature verification to obtain the authorized amount from Account A. The parameters include the owner granting authorization, the spender being authorized, the authorized amount, the signature deadline, and the owner’s signature data v, r, and s.
- Pay attention to every signature in on-chain interactions
Despite measures some wallets take to decode and display approve() authorization signature information, they provide almost no warning for permit() signature phishing, increasing the risk of attacks. Therefore, it is strongly recommended to rigorously inspect every unknown signature to ensure whether it is aimed at the permit() function.
- Separate the wallet for regular interaction from the wallet storing assets
This is extremely important for crypto users, especially airdrop hunters, as they interact with countless dApps or websites every day and are prone to traps. Storing only a small amount of funds in a wallet for regular interaction can keep losses within a manageable range.
7. Honeypot Attack
In the blockchain industry, a honeypot attack refers to a type of malicious token contract deployed by project teams. The contract only grants the project team permission to sell, while regular users can only buy instead of selling, thus suffering losses.
Here is an example. In an announcement on Telegram, Project A informs users that the token has been deployed on the mainnet and is available for trading. As the token can only be bought and cannot be sold, the price kept surging at first, and users who fear missing out keep buying. After sometime when users find it unable to sell, the project team seizes the opportunity and dumps the tokens, causing the price to plummet.
- _beforeTokenTransfer(): An internal function called during token transfers, which can only succeed when called by the owner; calls from other accounts will fail.
Use security scanning tools
- Token Sniffer for Ethereum tokens
- Ave Check for tokens on other chains
- Market websites with built-in detection tools like Dextools
Avoid trading tokens with low scores.
8. Front-Running Attack
Front-running originally emerged in traditional financial markets, where information asymmetry allowed financial intermediaries to gain profits by taking swift actions based on specific industry information. In the blockchain industry, front-running mainly stems from on-chain front-running, which involves manipulating miners to prioritize packing one’s own transactions onto the chain to gain profits.
In the blockchain field, miners can profit by manipulating the transactions they pack into blocks, e.g. excluding certain transactions and reordering transactions. Such profit can be measured with Miner Extractable Value (MEV). Before a user’s transaction is added to the Ethereum mainnet, the majority of transactions are aggregated in the mempool. Miners search for transactions with higher gas prices in this mempool and prioritize packing them to maximize their gains. Generally, transactions with higher gas prices are more easily packed by miners. Meanwhile, some MEV bots also scour the mempool for transactions with profitability.
Below is an example. Bill discovers a new hot token with significant price fluctuations. To ensure the success of token transactions on Uniswap, Bill sets an exceptionally wide slippage range. Unfortunately, Alice’s MEV bot detects this transaction in the mempool and promptly increases the gas fee, initiating a buy transaction before Bill’s and inserting a sell transaction after Bill’s within the same block. After block confirmation, this causes significant slippage losses for Bill, while Alice profits from an arbitrage operation of buying low and selling high.
The function is as below:
- solve(): A guessing function where anyone can submit an answer, and if the submitted answer matches the target answer, the submitter can receive 10 ethers.
- Bill finds the correct answer.
- Alice monitors the mempool, waiting for someone to submit the correct answer.
- Bill calls solve() to submit the answer and sets the gas price to 100 Gwei.
- Alice sees the transaction sent by Bill and discovers the answer. She sets a higher gas price than Bill’s 200 Gwei and calls solve().
- Alice’s transaction is packed by the miner before Bill’s.
- Alice wins a reward of 10 ethers.
The three major functions are as below:
- commitSolution(): A function to submit results, placing the user’s submitted answer solutionHash, submission time commitTime, and state revealed into the Commit structure.
- getMySolution(): A function to obtain results, allowing users to view their submitted answers and related information, including the user’s submitted answer solutionHash, submission time commitTime, and state revealed.
- revealSolution(): A function to claim rewards for guessing the puzzle, allowing users to claim rewards after providing the answer and the password they set.
- Bill finds the correct answer.
- Bill calls commitSolution() to submit the correct answer.
- In the next block, Bill calls revealSolution(), providing the answer and the password he set to claim the reward.
In commitSolution(), Bill submits an encrypted string, keeping the plaintext data submitted only to himself. In this step, the submission block time commitTime is also recorded. Next, in revealSolution(), the block time is checked to prevent front-running within the same block. Since calling revealSolution() requires the submission of the plaintext answer, this step aims to prevent others from bypassing commitSolution() and directly calling revealSolution(). After successful verification, the reward will be distributed if the answer is checked correct.
Smart contracts play a crucial role in blockchain technology and offer numerous advantages. Firstly, they enable decentralized, automated execution, ensuring transaction security and reliability without third parties. Secondly, smart contracts reduce intermediary steps and costs, enhancing transaction efficiency.
Despite so many benefits, smart contracts also face the risk of attacks that incur financial losses to users. As such, some habits are essential for on-chain users. Firstly, users should always carefully choose dApps for interaction and thoroughly review the contract code and related rules. Additionally, they should regularly update and use secure wallets and contract interaction tools to mitigate the risk of hacker attacks. Furthermore, it’s advisable to store their funds in multiple addresses to minimize potential losses from contract attacks.
For industry players, ensuring the security and stability of smart contracts is of equal importance. The first priority should be strengthening the auditing of smart contracts to identify and rectify potential vulnerabilities and security risks. Secondly, industry players should stay informed about the latest blockchain developments related to contract attacks and take security measures accordingly. Last but not least, they should also enhance user education and security awareness in terms of the correct use of smart contracts.
In conclusion, with the concerted efforts of both users and industry players, the security risks posed by smart contracts can be significantly mitigated. Users should always carefully select contracts and safeguard personal assets, while industry players should intensify contract auditing, stay abreast of technological advancements, and enhance user education and security awareness. Together, we will drive the secure and reliable development of smart contracts.
Solidity by Example
Blockchain Know-how of SlowMist
Chainlink – Top 10 DeFi Security Best Practices
WTF – Solidity 104 Contract Security
Vulnerabilities in DeFi Smart Contracts in 4 Categories with 38 Scenarios
Established in 2017, CoinEx is a global cryptocurrency exchange committed to making trading easier. The platform provides a range of services, including spot and margin trading, futures, swaps, automated market maker (AMM), and financial management services for over 5 million users across 200+ countries and regions. Since its establishment, CoinEx has steadfastly adhered to a “user-first” service principle. With the sincere intention of nurturing an equitable, respectful and secure crypto trading environment, CoinEx enables individuals with varying levels of experience to effortlessly access the world of cryptocurrency by offering easy-to-use products.