Smart contracts make transfer, delivery, and exchange tasks simpler and more convenient for users. They govern a huge percentage of current ICO and Ethereum projects, so smart-contract failures are a major concern for creators and investors.
You can read more about them in this article.
Smart Contracts Fail Description
However, smart contracts are only as smart as their creators, and contract design flaws often lead to bugs.
In terms of conducting interactions between multiple smart contracts, bugs can cause functions to execute from the user address instead of from the owner of the contract.
The National University of Singapore (NUS) has uncovered several severe smart-contract bugs. As a result, an analysis tool called Oyente was created for scanning smart contracts. Out of the 19,366 Ethereum smart contracts they analyzed, 8,833 of them had bugs!
For example, GitHub user Devops199 had an Ether transaction worth $285 million freeze because of a common bug. A new detection tool became necessary, and Maian was born. The Maian tool enabled a team of five cryptocurrency zealots to discover another 970,898 smart contracts that contained bugs.
A breakdown of the discoveries can be found in the table below:
Prodigal contracts transfer Ether or tokens to an incorrect wallet.
Suicidal contracts have a bug that makes the contract vulnerable to being killed by the owner.
Greedy contracts can be controlled by other users, resulting in frozen funds.
There are two other prominent smart-contract discovery tools called Mythril and NUS.
Let’s analyze the most common technical smart-contract pitfalls discovered over the past year.
Sending And Receiving Money/Tokens
Debugging a smart contract is not possible while sending and receiving money or tokens.
Compared to contracts developed for transfer orders, there are two possible implementations you can execute without a message call:
- “mine to” the contract address
- using selfdestruct(x).
When receiving Ether with no called function, you must have a backup function. If this function isn’t present, the Ether will be rejected.
During the execution of the function, the contract is only able to consume the “gas stipend” [2,300 gas] provided — but this amount of gas is not enough to access storage. To ensure success, always account for the gas requirements for function implementation.
It’s possible to get more gas using addr.call.value(x)() — similar to addr.transfer(x). With this function, the user can send more gas and get a wider range of functions. This helps to recover a “bad” code, allowing you to avoid having the errors spill into other functioning elements.
Using Withdrawal Patterns
The most popular method of sending funds is to use a withdrawal pattern instead of a contract. A customer can also send Ether through a direct send call. It’s best to avoid direct calls, however, because they can compromise security.
Consider this example:
Sending a great amount of money to a contract in order to become the “richest.”
Here is the other method of sending the pattern:
In this case, an attacker can trigger a failure through the richest contract address backup function and put the contract into an ‘injured’ state.
This can be instigated by invoking revert() or by spending more than the 2,300 gas stipend. As a result, delivering funds to an ‘injured’ contract won’t be successful, and funds can theoretically be stuck in this state indefinitely.
The first “withdraw” pattern only causes a withdrawal failure, and won’t harm the rest of the contract’s working process.
The best way to minimize loss is to restrict the amount of Ether (or other tokens) in a smart contract.
External Function Calls
Another common smart-contract failure cause is an instant working crash of external function calls. Hackers can increase the value of the call stack before interacting with your contract.
Remember that .send(), .call(), .callcode() and .delegatecall() functions all work the same way.
Make sure not to use tx.origin for authorization.
Let’s imagine this is your wallet:
and some person encourages you to send Ether to a suspicious wallet:
If the authorization of the wallet was checked by msg.sender, it would send the Ether to the wrong wallet, not to the rightful owner.
The tx.origin function causes it to select the address of the wallet to initiate the transaction. As a result, the hacker’s wallet address will take all of your funds.
Checking the Return Value of a Send Method
A lot of contracts do not check the return value or gas levels during a transaction. Remember, if the stack depth is more than 1,024, or if the gas runs out, the transfer is likely to fail.
To minimize the potential of loss, make sure to check the return value, and use a transfer or a pattern to allow the recipient to withdraw funds.
It’s worth mentioning that data about transaction content and state variable are accessible to everyone, even if it is marked private.
Encryption can help you avoid this issue, but if the data can be read, it’s still possible.
By default, access to a contract reading can be restricted by other contracts. You can easily change it by making the state public.
It’s also possible to restrict the number of people who can edit the contract’s state and use its functions.
Here are some general recommendations regarding source-code quality:
- Restrict the number of local variables.
- Restrict the length of functions.
- Document functions to make your intentions understandable for other people. This will help to determine if the action of the code differs from your personal code.
Consider this hypothetical scenario:
An interaction between contract (A) with contract (B) gives control to contract (B). Contract B can call back into A until the operation is finished.
The code below provides an example of a related bug that can crop up because of the limited gas required to send:
The problem lies within the following:
The Ether transfer process contains the code execution. The recipient’s contract can call back into withdrawal, making it possible to refund several times and possess all the contract’s Ether.
Use the following Checks-Effects-Interactions pattern to avoid such situations.
All checks regarding who called the function — if there are arguments in range, whether enough Ether was sent, or whether the person has tokens — should be done from the very beginning.
After that, make changes to the state variables of the contract. Consider interactions among the contracts at the end of any function.
In the past, in order to be in good standing, a contract had to wait for external function calls. That is one of the most significant effects of re-entrancy. Calls to the appropriate contracts can also cause calls to non-planned contracts.
Gas Limit and Loops
Be careful with loops that don’t have a defined number of iterations, or ones that have loops that could be influenced by storage values. The amount of gas is restricted so that transferring consumes a specific amount of it.
In any case, the number of iterations in a loop might increase the gas limit. This can cause a freeze at a certain point. However, it may not be related to constant read data functions. These functions can be called by other contracts or other connected operations and interrupt their activity. Please be vigilant about such cases in the documentation of your contracts.
Removing an intermediary will grant you some of the benefits of a new code. Providing new self-checking functions to a smart contract can help determine whether any Ether has leaked or if the sum of the tokens is the same as the balance of the contract. Gas limitations can also be calculated throughout the chain.
If a check detects a problem with the contract, it defaults to “failsafe” mode. In this case, many functions become unavailable, and control is given to a third party, or the contract demands the funds back.
ATTENTION! SUICIDE Function
An important example of the suicide function is when a token freezes on all Parity multisig wallets.
One high-profile incident involved a failure that occurred due to careless handling of the library’s code, rendering approximately $280 million of Ether inaccessible.
The user conducted only two transactions:
- initWallet function – to vary the owner address state
The main causes were:
- The wallet could be initialized only once.
- The function could only be implemented by an uninitialized modifier.
The Wallet Library is not equipped with wallet functions. The code was transferred to other contracts as a DELEGATE CALL.
2. The “kill” transaction
The kill transaction froze all wallets that were connected to the library code.
So, how do you discover these bugs ahead of time?
- Provide an inventory of all SSTORE instructions that might be called by anyone.
- Check the information about the SUICIDE function, and try to solve all existing and potential problems.
- If you have found that some indexes of SSTORE are vulnerable, fix this by accessing the SUICIDE block.
This unprotected SUICIDE call facilitated the Parity catastrophe, and could have been solved using Mythril.
With formal verification, the customer makes sure that the source code carries out an appropriate formal specification.
This helps determine the difference between the specification you had and the process you carried out. In this way, you can avoid critical mistakes.
To implement formal verification, you need to:
- Provide an audit of all arithmetic operations with user-supplied data
- Verify all working elements before the arithmetic operations
- Use the safe-math library to be sure that all functions work appropriately, and to see if there are any overflows.
Despite all of the above-mentioned risks, there is a solution.
Contact the skilled, seasoned Applicature team to provide a professional, low-risk smart contract for your project.