Solidity Vulnerabilities
Solidity Vulnerabilities¶
https://github.com/solidified-platform/audits/blob/master/Audit%20Report%20-%20OpenSea%20Contract%20Creator%20%5B08.08.2021%5D.pdf
Reentrancy¶
A software routine is considered reentrant if it can be interrupted mid-execution and be safely called again before its previous invocation completes execution. In Ethereum, reentrancy bugs occur when an attacker is able to obtain control flow of a smart contract and "reenter" it, by calling another function before a previous function call returns. The function called can either be the same as the original one or a different one. This can be turned into a security vulnerability when in doing do, validation logic is bypassed and some sort of unsafe state change occurs (e.g. the attacker gets paid out twice).
Control flow may be hijacked whenever an untrusted, external contract is called using the CALL opcode and enough gas is supplied. In Solidity, the CALL opcode is used when invoking functions such as
- call
- callcode
- delegatecall
- transfer
- send.
However, the transfer and send functions purposely supply a very small amount of gas when making a call to the external contract – just enough for the callee to log an event. Therefore, we are only concerned about uses of the functions call, callcode, and delegatecall.
Example Code¶
function cashOut() {
if (!msg.sender.call.value(balances[msg.sender])()) {
throw;
}
balances[msg.sender] = 0;
}
Additional Resources¶
- http://hackingdistributed.com/2016/07/13/reentrancy-woes/
- https://medium.com/@gus_tavo_guim/reentrancy-attack-on-smart-contracts-how-to-identify-the-exploitable-and-an-example-of-an-attack-4470a2d8dfe4
- https://medium.com/@JusDev1988/reentrancy-attack-on-a-smart-contract-677eae1300f2
Missing Authorization/Validation¶
Just like traditional applications, dApps are also affected by authorization issues and other forms of missing validation. A common Solidity development pattern is to extensively use function modifiers to perform validation routines. These modifiers may check that the sender's address is whitelisted, or that the contract is in a particular state before the function is allowed to be called. However, it is up to developers to remember to consistently use function modifiers.
A related issue can arise with library contracts, which are deployed for other contracts to DELEGATECALL to and are not intended to be used directly. In this scenario, a library contract should still be fully initialized and have proper authorization enforced. Otherwise, it may be possible for an attacker to take over and destroy the library contract. An example of this happening is Parity's multisig contract. This effectively caused a complete denial of service, since all contracts dependent on Parity's multisig library were no longer able to successfully DELEGATECALL to it.
Example Code¶
pragma solidity ^0.4.24;
contract MissingAuth {
uint256 public price;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
constructor() public {
owner = msg.sender;
}
function changePrice(uint256 _price) onlyOwner public {
price = _price;
}
function changeOwner(address _owner) public {
owner = _owner;
}
}
Additional Resources¶
Transaction-Ordering Dependence¶
Also known as "front running", attacks using transaction-ordering dependence (TOD) exploit the fact that the order of transactions within a block can be easily manipulated. This is possible due to the following reasons:
- It is possible to observe pending transactions that haven't been committed to a block yet.
- Most miners configure their nodes to order transactions by gas price, which can easily be changed by clients to influence transaction ordering.
- Clients can attempt to cancel pending transactions by resending a transaction with the same nonce and a higher gas price. The second transaction would likely take precedence over the first transaction due to transaction ordering (see previous bullet point).
This can be especially problematic for applications such as decentralized markets, where participants getting to peak at future transactions could be used to commit market manipulation. Unfortunately, there is no simple way to address this issue from a functional perspective. Application design must make use of schemes such as hash-commit-reveal (e.g. each participant generates a number and publishes its hash to "commit" to the value, and then everybody reveals their values later) in order to ensure that participants cannot cheat.
Example Code¶
Additional Resources¶
- https://hackernoon.com/front-running-bancor-in-150-lines-of-python-with-ethereum-api-d5e2bfd0d798
- https://medium.com/@matt.czernik/on-blockchain-frontrunning-part-i-cut-the-line-or-make-a-new-one-b33850663b55
- http://hackingdistributed.com/2017/08/28/submarine-sends/
- http://swende.se/blog/Frontrunning.html
Timestamp Dependence¶
Timestamps can be slightly modified by miners and should only be treated as accurate up to 30 seconds. This is because miners are only required to post a timestamp within 30 seconds of block validation. All uses of block.timestamp and now should be reviewed. If the functionality in question cannot tolerate a 30 second drift in time, it is not safe to use a timestamp.
Timestamps should never be relied on for generating randomness, which is where this issue often appears (see Bad Randomness). In addition, it is unsafe to assume that the frequency of block creation will always remain the same. The average difference between block timestamps is subject to change due to network issues, hard forks and difficulty bombs.
Example Code¶
The following code snippet is taken from a here, an example of a real (now defunct) vulnerable contract.
// Compute some *almost random* value for selecting winner from current transaction.
var random = uint(sha3(block.timestamp)) % 2;
Additional Resources¶
Integer Overflows and Underflows¶
Solidity's integers are subject to overflow and underflows, which can be exploited to intentionally cause unexpected changes in values (e.g. an address ends up with a token balance of 2256 - 1). As such, a sanity check should always be performed on the result of integer arithmetic. Most smart contracts use SafeMath to accomplish this, which throws when an overflow or an underflow is detected.
Special attention should be placed on contracts that manipulate the length of dynamic arrays, which are always stored sequentially in a contract's storage. If an overflow or underflow were to occur in a dynamic array, it may be possible for an attacker to overwrite arbitrary chunks of contract storage.
Overflows and underflows are also concerns when inserting user input into call data. The most famous case of this to date is the ERC20 short address attack.
The severity of this vulnerability is dependent on how likely an overflow or underflow is to occur. None the less, it should always be reported.
Example Code¶
mapping (address => uint256) balances;
function transfer(address _to, uint256 _value) {
require(balances[msg.sender] – _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
}
Additional Resources¶
- https://blog.matryx.ai/batch-overflow-bug-on-ethereum-erc20-token-contracts-and-safemath-f9ebcc137434
- https://ethereumdev.io/safemath-protect-overflows/
- https://ericrafaloff.com/analyzing-the-erc20-short-address-attack/
Denial of Service¶
Contracts that are intentionally broken may never be able to recover due to the immutable nature of the Ethereum blockchain. Therefore, it is extremely important that developers strive for making their contracts as bug-free as possible. Even a seemingly innocuous bug may be abused by an attacker in a creative way in order to render a contract unusable, thereby locking up all of the held Ether and tokens.
Example Code¶
Additional Resources¶
Contract Balance Manipulation¶
Ether can always be forcibly sent to a contract, regardless of if a fallback function is payable. This can be done by having another contract call suicide(victimAddress), which sends all of that contract's current balance to victimAddress. Therefore, special attention should be given to the review of any calculations or validation routines that incorporate a contract's current balance. If it is assumed that a contract's balance may remain correct, it may be possible to subvert validation or cause an integer underflow.
Example Code¶
Additional Resources¶
Missing Error Handling¶
Solidity functions such as send are expected to return true or false, depending on the success of an operation, rather than throw an exception on failure. It is important for contracts to check these return values and implement error handling routines. If a contract doesn't do so, other functionality may become broken (which can lead to a denial of service), or validation checks may no longer function properly (which can lead to theft of tokens or Ether).
Example Code¶
The following example demonstrates that if send were to fail, the value of totalEtherSupply would be changed regardless and become incorrect. This could cause other contract functionality to no longer behave as expected.
// If the sender paid too much, refund them
if (etherSent > currentPrice) {
uint256 amountToReturn = etherSent - currentPrice;
msg.sender.send(amountToReturn);
totalEtherSupply -= - amountToReturn;
}
Additional Resources¶
- http://hackingdistributed.com/2016/06/16/scanning-live-ethereum-contracts-for-bugs/
- https://www.kingoftheether.com/postmortem.html
Bad Randomness¶
Random number generation in a deterministic system is extremely difficult, and is currently a hot topic of academic research. If a contract attempts to generate random numbers and it isn't using a generally accepted method of doing so, there's a very good chance it is flawed.
Generally accepted methods of producing random numbers include the following:
- Using RANDAO
- Bitcoin block headers (verified through BTCRelay)
- Hash-commit-reveal schemes
- Avoid relying on randomness for on-chain computation all together
Common sources of bad entropy include:
- block.coinbase
- block.difficulty
- block.number
- block.blockhash()
- block.blockhash(block.number)
- block.blockhash(block.number - 1)
- block.timestamp
Example Code¶
The following code snippet is taken from a here, an example of a real (now defunct) vulnerable contract.
// returns random number from 0 to 51
// let's say 'value' % 4 means suit (0 - Hearts, 1 - Spades, 2 - Diamonds, 3 - Clubs)
// 'value' / 4 means: 0 - King, 1 - Ace, 2 - 10 - pip values, 11 - Jacket, 12 - Queen
function deal(address player, uint8 cardNumber) internal returns (uint8) {
uint b = block.number;
uint timestamp = block.timestamp;
return uint8(uint256(keccak256(block.blockhash(b), player, cardNumber, timestamp)) % 52);
}
Additional Resources¶
- https://blog.positive.com/predicting-random-numbers-in-ethereum-smart-contracts-e5358c6b8620
- https://blog.gdssecurity.com/labs/2018/6/1/breaking-randomness-in-the-ethereum-universe-part-1.html
Storage of Secrets¶
All contract storage is easily inspectable, regardless of if a variable is denoted as public in source code. Therefore, contracts should never store secrets.
Example Code¶
The following code snippet is taken from a here, an example of a real (now defunct) vulnerable contract.
//Generate random number between 0 & max
uint256 constant private FACTOR = 1157920892373161954235709850086879078532699846656405640394575840079131296399;
function rand(uint max) constant private returns (uint256 result){
uint256 factor = FACTOR * 100 / max;
uint256 lastBlockNumber = block.number - 1;
uint256 hashVal = uint256(block.blockhash(lastBlockNumber));
return uint256((uint256(hashVal) / factor)) % max;
}
Additional Resources¶
Unsafe Functionality¶
Code with unintended side-effects can cause smart contracts to become stuck (see Denial of Service) or storage to become altered. For additional information, see the example code below.
Example Code¶
// Example 1. Confusing type deduction
//
// Note that `i` is of type uint8. If the length of `users` ever exceeds 255, `i` will overflow
// before the for loop would ever return.
for(var i = 0; i < users.length; i++) {
//...
}
// Example 2. Faulty array manipulation
//
// Note that the length of `users` is reduced by 1 regardless of if an element from the array
// was actually found and removed.
address[] users;
function removeUser(address user) {
uint j;
var oldUsers = users;
// overwrite the array with all users minus the user we want to remove
for(uint i = 0; i < users.length; i++) {
if (oldUsers[i] != user) {
users[j] = oldUsers[i];
j++;
}
}
// reduce the size of the array by 1 to reflect the removed user
users.length--;
}
Additional Resources¶
Misc "Gothas"¶
The following code demonstrates confusion that can arise when declaring a variable of an array type inside of a function. Despite being declared as an array of ints,`b` is actually treated as an uninitialized storage pointer that points to the beginning of the contract's storage. Calling `Add()` will overwrite the value of `a` with the returned value of `push()`, which is the length of `b`.
pragma solidity ^0.4.24;
contract MyContract {
uint8 public a;
function Add() public {
int[] b;
b.push(1);
}
function Sub() public {
int[] b;
b.length--;
}
}
Deprecated Vulnerabilities¶
The following vulnerabilities have been successfully fixed or mitigated in the latest version of Ethereum:
- Call Depth Attack - Mitigated by EIP 150.
Security¶
https://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/