30 most common smart contract vulnerabilities
Void constructor Calls to unimplemented base contract constructors lead to incorrect assumptions. If the constructor is not implemented, remove the call.
Using a single Solidity compiler version across all contracts is preferable rather than using different multiple versions that have different flaws and security checks.
A MITM malicious contract may abuse the use of tx.origin for authorisation by diverting calls from the normal user who interacts with it. Instead, use msg.sender.
Address checks (e.g., owner, controller, etc.), which are often in modifiers, should be used to ensure adequate access control on contract functions that execute critical logic. Bypassing checks implies attackers can take control of crucial logic.
Adopting very old Solidity versions hinders you from receiving the benefits of bug patches and improved security checks. Contracts could be vulnerable to unforeseen compiler issues if they aren’t using the most recent versions.
Sending Ether or tokens to user-controlled addresses via unprotected (external/public) function calls could enable users to withdraw unauthorized funds.
Unprotected call to self-destruct: A user or an attacker could unintentionally or maliciously terminate the contract. Preserve access to these functions.
Modifiers shouldn’t perform state changes or external calls, as this goes against the checks-effects-interactions design pattern. They should just implement checks. Developers and auditors may not be aware of these side effects since the modifier code is frequently separated from the function implementation.
Incorrect modifier: A function utilizing a modifier will return the default value if it does not execute or revert, which might lead to unexpected behavior.
Until solc 0.4.22, constructor names had to be the same as the contract class that contained it. It is not a constructor if it is misnamed, which has security consequences. The constructor keyword was introduced in Solc 0.4.22. Contracts may have both old-style and new-style constructor names before solc 0.5.0, with the first defined one taking precedence over the second if both existed, which caused security difficulties. The constructor keyword was required in Solc 0.5.0.
By using delegatecall() or callcode() to an address that the user controls, it is possible for malicious contracts to run in the context of the caller’s state. Make sure to use trusted destination addresses for these kinds of calls.
Untrusted external contract calls may callback, resulting in unforeseen outcomes like multiple withdrawals or out-of-order events. Use reentrancy guards or the check-effects-interactions pattern.
ERC777 callbacks and reentrancy: Token transfers that use ERC777 can trigger arbitrary callbacks using hooks. If reentrancy guards are not used, malicious contract addresses may result in reentrancy on such callbacks.
Despite the fact that transfer() and send() only forward 2300 gas, they have been suggested as a security best practice to thwart reentrancy attacks, the gas repricing of opcodes may cause deployed contracts to malfunction. For reentrancy protection, use call() rather than hardcoded gas limitations, the checks-effects-interactions pattern, or reentrancy guards.
Marking variables as private does not prevent them from being read. Private information ought to be kept off-chain or encrypted rather than unencrypted in contract code or state.
Locking the pragma prevents contracts from inadvertently being released using an outdated(old) compiler version with unresolved problems. Contracts should be deployed using the same compiler version/flags that they were tested with.
Avoid using PRNGs that rely on block.timestamp, now, or blockhash because they can be somewhat tampered with by miners.
Because of synchronization issues, miner manipulation, and unpredictable block times, the block.timestamp and block.number are poor proxies (representations, not to be confused with smart contract proxy/implementation pattern) for time.
Failure to use SafeMath from OpenZeppelin (or comparable libraries) that check for overflows or underflows may result in vulnerabilities or unexpected behavior if a user or attacker has access to the integer operands of such arithmetic operations. For all arithmetic operations, Solc v0.8.0 added default overflow/underflow checks.
Divide before multiplying: Since Solidity integer division may truncate, it is often preferable to do multiplication before division to prevent precision loss.
By keeping an eye on the mempool, race conditions can be imposed on particular Ethereum transactions. This technique, for instance, can be used to front-run the traditional ERC20 approve() update. Don’t assume anything regarding transaction order dependence.
Replay attacks may result from the ecrecover function’s susceptibility to signature malleability. Use the ECDSA library from OpenZeppelin.
Contracts interacting with such functions that were built with solc >= 0.4.22 will revert. Use the SafeERC20 wrappers from OpenZeppelin.
Contracts interacting with ERC721 ownerOf() that returns a bool rather than an address type and were built with solc >= 0.4.22 will revert. Use the ERC721 contracts from OpenZeppelin.
Self-destruct(), coinbase transactions, payable functions, and pre-sent before creation are all ways that a contract can get Ethereum. Thus, it is possible to manipulate contract logic that depends on this.Balance.
Check that all safety measures and nuances of the fallback and receive functions relating to visibility, state mutability, and Ether transfers have been taken into account.
Strict equalities are risky because they can unintentionally or maliciously lead to unexpected behavior when used with tokens or ether. Depending on the contract logic, you might want to consider using >= or = in place of == for similar variables.
Contracts that take ether through payable functions but do not include withdrawal mechanisms will lock up the ether. Taking off the payment attribute or adding a withdraw function would be helpful.
If a contract does not have access to the source code during construction, it may be able to circumvent the extcodesize check used to identify whether a call originated from an EOA or a contract account. Another option is to check whether tx.origin == msg.sender. Both have implications that must be explored.
Removing a struct that contains a mapping does not erase the mapping contents, which may have unanticipated results.
Subscribe to my newsletter
Read articles from Harbor directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by