Security Research
Smart Contract security research will reside here!
Function Modifiers
I found really useful example during my research on advanced solidity in smart contract which is taken from https://cryptozombies.io/en/lesson/3/chapter/3 . I'm glad to sharing with you guys!
A function modifier looks just like a function, but uses the keyword modifier
instead of the keyword function
. And it can't be called directly like a function can — instead we can attach the modifier's name at the end of a function definition to change that function's behavior.
Let's take a closer look by examining onlyOwner
:
Notice the onlyOwner
modifier on the renounceOwnership
function. When you call renounceOwnership
, the code inside onlyOwner
executes first. Then when it hits the _;
statement in onlyOwner
, it goes back and executes the code inside renounceOwnership
.
So while there are other ways you can use modifiers, one of the most common use-cases is to add quick require
check before a function executes.
In the case of onlyOwner
, adding this modifier to a function makes it so only the owner of the contract (you, if you deployed it) can call that function.
Note: Giving the owner special powers over the contract like this is often necessary, but it could also be used maliciously. For example, the owner could add a backdoor function that would allow him to transfer anyone's zombies to himself!
So it's important to remember that just because a DApp is on Ethereum does not automatically mean it's decentralized — you have to actually read the full source code to make sure it's free of special controls by the owner that you need to potentially worry about. There's a careful balance as a developer between maintaining control over a DApp such that you can fix potential bugs, and building an owner-less platform that your users can trust to secure their data.
Known Attacks
Race Conditions
One of the major dangers of calling the external contract is that they can take over the control flow, and make changes to your data that the calling function wasn't expecting.
Reentrancy
Problem: The involved function that could be called repeatedly.
Insecure code:
Analysis: Since the user's balance is not set to 0 until the very end of the function, the second (and later) invocations will still succeed, and will withdraw the balance over and over again.\
Solution: The best way to avoid the problem is to use send() instead of call.value()(). This will prevent any external code from being executed.
Best practice:
Be aware of the tradeoffs between send(), transfer(), and call.value()
When sending ether be aware of the relative tradeoffs between the use of someAddress.send(), someAddress.transfer(), and someAddress.call.value()().
- someAddress.send()and someAddress.transfer() are considered safe against reentrancy. While these methods still trigger code execution, the called contract is only given a stipend of 2,300 gas which is currently only enough to log an event.
- x.transfer(y) is equivalent to require(x.send(y));, it will automatically revert if the send fails.
- someAddress.call.value(y)() will send the provided ether and trigger code execution. The executed code is given all available gas for execution making this type of value transfer unsafe against reentrancy.
Using send() or transfer() will prevent reentrancy but it does so at the cost of being incompatible with any contract whose fallback function requires more than 2,300 gas. It is also possible to use someAddress.call.value(ethAmount).gas(gasAmount)() to forward a custom amount of gas.
One pattern that attempts to balance this trade-off is to implement both a push and pull mechanism, using send() or transfer() for the push component and call.value()() for the pull component.
It is worth pointing out that exclusive use of send() or transfer() for value transfers does not itself make a contract safe against reentrancy but only makes those specific value transfers safe against reentrancy.
Informative answer from Stackoverflow
throws on failure
forwards
2,300
gas stipend (not adjustable), safe against reentrancyshould be used in most cases as it's the safest way to send ether
returns
false
on failureforwards
2,300
gas stipend (not adjustable), safe against reentrancyshould be used in rare cases when you want to handle failure in the contract
returns
false
on failureforwards all available gas (adjustable), not safe against reentrancy
should be used when you need to control how much gas to forward when sending ether or to call a function of another contract
However, if you can't remove the external call, the next simplest way to prevent this attack is to make sure you don't call an external function until you've done all the internal work you need to do:
Note that if you had another function which called withdrawBalance(), it would be potentially subject to the same attack, so you must treat any function which calls an untrusted contract as itself untrusted. See below for further discussion of potential solutions.
public - all can access
external - Cannot be accessed internally, only externally
internal - only this contract and contracts deriving from it can access
private - can be accessed only from this contract
Cross-function Race Conditions
Problem: Similar problem, An attacker may also be able to do a similar attack using two different functions that share the same state.
Insecure code:
Pitfalls in Race Condition Solutions
Since race conditions can occur across multiple functions, and even multiple contracts, any solution aimed at preventing reentry will not be sufficient.
Instead, we have recommended finishing all internal work first, and only then calling the external function. This rule, if followed carefully, will allow you to avoid race conditions. However, you need to not only avoid calling external functions too soon, but also avoid calling functions which call external functions. For example, the following is insecure:
Even though getFirstWithdrawalBonus() doesn't directly call an external contract, the call in withdrawReward() is enough to make it vulnerable to a race condition. You therefore need to treat withdrawReward() as if it were also untrusted.
Another solution often suggested is a mutex. This allows you to "lock" some state so it can only be changed by the owner of the lock. A simple example might look like this:
// Note: This is a rudimentary example, and mutexes are particularly useful where there is substantial logic and/or shared state mapping (address => uint) private balances; bool private lockBalances;
If the user tries to call withdraw() again before the first call finishes, the lock will prevent it from having any effect. This can be an effective pattern, but it gets tricky when you have multiple contracts that need to cooperate. The following is insecure:
!!!Important: An attacker can call getLock(), and then never call releaseLock(). If they do this, then the contract will be locked forever, and no further changes will be able to be made. If you use mutexes to protect against race conditions, you will need to carefully ensure that there are no ways for a lock to be claimed and never released. (There are other potential dangers when programming with mutexes, such as deadlocks and livelocks. You should consult the large amount of literature already written on mutexes, if you decide to go this route.)
Overflow & Underflow
Be careful with the smaller data-types like uint8, uint16, uint24...etc: they can even more easily hit their maximum value.
Overflow Attack[edit]
An overflow occurs when a number gets incremented above its maximum value. Suppose we declare an uint8 variable, which is an unsigned variable and can take up to 8 bits. This means that it can have decimal numbers between 0 and 2^8-1 = 255.
Keeping this in mind, consider the following example.
This will lead to an overflow because a’s maximum value is 255.
Solidity can handle up to 256-bit numbers. Incrementing by 1 would to an overflow situation:
This will lead to an overflow, because a’s maximum value is 255.
Solidity can handle up to 256 bit numbers. Incrementing by 1 would to an overflow situation
POC:
Solutions:
Underflow Attack
First things first, let’s make sure we understand what an uint256 is. A uint256 is an unsigned integer of 256 bits (unsigned, as in only positive integers). The Ethereum Virtual Machine was designed to use 256 bits as its word size, or the number of bits processed by a computer’s CPU in one go. Because EVM is limited to 256 bits in size, the assigned number range is 0 to 4,294,967,295 (2²⁵⁶). If we go over this range, the figure is reset to the bottom of the range (2²⁵⁶ + 1 = 0). If we go under this range, the figure is reset to the top end of the range (0–1= 2²⁵⁶).
Underflow takes place when we subtract a number greater than zero from zero, resulting in a newly assigned integer of 2²⁵⁶. Now, if an attacker’s balance experiences underflow, the balance would be updated such that all funds could be stolen.
The Attack
The attacker initiates the attack by sending 1 Wei to the target contract
The contract credits the sender for funds sent
A subsequent withdrawal of the same 1 Wei is called
The contract subtracts 1 Wei from the sender’s credit, now the balance is zero again
Because the target contract sends ether to the attacker, the attacker’s fallback function is also trigger and a withdrawal is called again
The withdrawal of 1 Wei is recorded
The balance of the attacker’s contract has been updated twice, the first time to zero and the second time to -1
The attacker’s balance is reset to 2²⁵⁶
The attacker completes the attack by withdrawing all of the funds of the targeted contract
To avoid falling victim to an underflow attack, best practice is to check if the updated integer stays within its byte range. We can add a parameter check in our code to act as a last line of defense. The first line of function withdraw() checks for adequate funds, the second checks for overflow, and the third checks for underflow.
Solution: updates the user’s balance BEFORE sending funds, as discussed earlier.
Constantinople Upgrade Security Research
Introduction
The upcoming Constantinople Upgrade for the ethereum network introduces cheaper gas cost for certain SSTORE operations. As an unwanted side effect, this enables reentrancy attacks when using address.transfer(...) or address.send(...) in Solidity smart contracts. Previously these functions were considered reentrancy-safe, which they aren’t any longer.
Vulnerable Smart Contract
Link: https://github.com/ChainSecurity/constantinople-reentrancy
Purpse of the contract: It simulates a secure treasury sharing service. Two parties can jointly receive funds, decide on how to split them, and receive a payout if they agree*
Why this contract is vulnerable?. My understanding is the contract uses operation within transfer function
Moreover the splits[id]' is dependent on updateSplit function. Therefore, the reetrancy happens at this point. The attacker can make another contract and call the updateSplit function and execute splitFunds as they want. This can be done as the following steps in the Attacker Case section.
Attack case
An attacker will create such a pair with where the first address is the attacker contract listed below and the second address is any attacker account. For this pair, the attacker will deposit some money.
1. The attacker sets the current split using updateSplit in order to make sure that the update later will be cheap. This is the effect of the Constatinople upgrade. The attacker sets the split in such a way that his first address (the contract) is supposed to receive all of the funds.
2. The attacker contract calls the splitFunds function, which will perform the checks*, and send the full deposit of this pair to the contract using a transfer.
3. From to the fallback function, the attacker updates the split again, this time assigning all funds to his second account.
4. The execution of splitFunds continues and the full desposit is also transferred to the second attacker account.
Conditions
1. There must be a function A, in which a transfer/send is followed by a state-changing operation. This can sometimes be non-obvious, e.g. a second transfer or an interaction with another smart contract.
2. There has to be a function B accessible from the attacker which (a) changes state and (b) whose state changes conflict with those of function A.
3. Function B needs to be executable with less than 1600 gas (2300 gas stipend - 700 gas for the CALL).
Solutions
Check if our contract is vulnerable regarding this attack
(a) check if there are any operations following a transfer event.
(b) check if those operations change storage state, most often by assigning some storage variable. If you are calling another contract, e.g. a tokens transfer method, check which variables are modified. Make a list.
(c) check if any other method accessible from non-admins in your contract uses one of these variables
(d) check if these methods change storage state themselves
(e) check if the method is below 2300 in gas, keeping in mind that SSTORE operations are potentially only 200 gas.
If all of this is the case, it is likely that an attacker can cause your contract to get into an undesirable state. Overall, this is another reminder why the Checks-Effects-Interactions Pattern is so important. (https://solidity.readthedocs.io/en/v0.5.2/security-considerations.html#use-the-checks-effects-interactions-pattern)
This is my solution, this should fix the issue for afore vulnerable smart contract.
The splits[id] is now initialized to a certain variable. Because whenever you make .transfer() or .send() to an address that is a contract, you are calling its fallback method, a reentrancy attack is when an attacker makes a contract where the fallback method calls the contract again, possibly taking money again if the state isn't updated before the call.
Lesson learned
After Constantinople, storage operations which are changing “dirty” storage slots cost only 200 gas. To cause a storage slot to be dirty, it has to be changed during the ongoing transaction. As shown above, this can often be achieved by an attacker contract through calling some public function which changes the required variable. Afterwards, by causing the vulnerable contract to call the attacker contract e.g. with msg.sender.transfer(...) the attacker contract can use the 2300 gas stipend to manipulate the vulnerable contract’s variable successfully.
I admitted that this does not mean that .transfer() and .send() all of the sudden vulnerable to reentrancy attack, a lot of things have to line up in order to execute this kind of an attack as I've just described on afore Conditions section; However, we should re-pen testing our smart contract that already went live, be said by chainsecurity - A scan of the main ethereum blockchain using the data available from eveem.org did not uncover vulnerable smart contracts. We are working together with members of the ethsecurity.org working group to expand this scan to the complex smart contracts which haven’t been decompiled yet. Especially decentralized exchanges which frequently call ether transfer functions to untrusted accounts followed by state changes afterwards might be vulnerable .
Last updated