Ethereum Smart Contracts: Security Starts with Architecture
When you create Blockchain, you should decide on the right platform to deploy it. While Blockchains are secure just by their properties, there can be some known vulnerabilities that can exist in your architecture and they can be manipulated by hackers and wrong-doers.
The architecture you use should have integrated security features that can do the following:
- Prevent anyone from accessing root and sensitive information. This includes even administrators and root users.
- Any illicit attempt to alter the data should be thwarted. Unauthorized access should be denied and reported immediately.
- Encryption keys should be carefully guarded using high-security standards. This will ensure that the keys are not misappropriated.
When your architecture has these features, your network will get the additional protection that’s needed to avert any attacks — whether they are from inside or outside.
Test checklist
If you want to test a Blockchain project, how do you do it? And what limitations are there in this technology? Before we delve into these topics, keep in mind that Blockchain isn’t limited to Bitcoin. While Bitcoin is also a Blockchain based technology, Blockchain in itself is a much broader term. Let’s go through the parameters for testing.
Block size limit
For Ethereum block size limit means the same as the gas size limit. The gas limit is a cap on both processing and storage because the cost of a transaction is fixed in units of gas for each type of instruction.
The upper gas limit of a block is 1MB. When Bitcoin was introduced, for the first 18 months, the average block size was under 30KB. Towards the end of 2017, it was around 1MB. It’s not yet decided what will happen once the block size reaches over 1MB. Since there can be multiple transactions in a single block, the block size can increase.
Load
Since there are many people on the Blockchain, the load is an important parameter to be tested. If we take the example of Bitcoin, there are about 3–4 transactions per second. This works as of now, but what if the load increases? It’s important to maintain the performance of the Blockchain even under heavy loads.
Performance
Performance can go down with load. The performance of the Blockchain should be optimal and it shouldn’t take too long for a query to return with results. If a query has to go through every block in the chain, how long will it take? This should be considered while developing the project.
Collision
What happens when there is a collision? If there is a split in the chain, what action will be taken? The chain is peer reviewed and there should be a prompt action to decide which branch of the chain will continue and which will be discarded. There can be other instances when a collision occurs in the chain. The system should be able to handle exceptional conditions effectively.
Security
Since a Blockchain always has many miners, security is always a major issue. Also, security can be a complex parameter since the Blockchain technology needs a multi-layered security system. Any instantaneous transactions cannot be stopped even if a layer gets hacked. It’s important to test the layers and to make sure that one security layer is not affected by the others.
Here are some other parameters that quality assurance specialists should check while testing the Ethereum Smart Contract.
Investigation dependency Gas Usage from Amount of Investors/Website operators
Gas refers to a fee paid for a transaction. Each transaction can’t be done for free and needs some capacity (hardware, power, maintenance etc.) in order to be executed.
This way we have a price for every transaction which is calculated in Gas if we talk about Ethereum operations.
Since Ethereum itself has no correlation with ever-changing infrastructure prices, gas is a so-called equivalent for the transaction price which should be approved by the investors.
Users can also set limits for the gas amount to be transferred to avoid users’ wallets going empty because of multiple transactions taking place simultaneously.
In the test cases below we aimed at finding the maximum amount of investors and optimal gas price for a transaction to be performed.
Gas usage for 10 investors:
Gas usage for 50 investors:
Gas usage for 100 investors:
Gas usage for 150 investors:
Gas usage for 200 investors:
Gas usage for 250 investors:
Gas usage for 251 investors:
The investigation process described above resulted in the fact, that the transaction limit is 250 investors per one transaction.
The Security Checklist Criteria
It’s a common mistake to rely on a single random testing tool and hope you’ve delivered a secure Ethereum-based solution. Instead, our expert development team has worked out a list of key parameters a truly secure smart contract should meet:
- Confusion of the different method calling possibilities: send(), transfer(), and call.value()
- Missing error handling of external calls
- Erroneous control flow assumptions after external calls
- The use of push over pull for external calls
- Lack of enforcement of invariants with assert() and require()
- Rounding errors in integer division
- Fallback functions with a higher gas limit than 2300
- Functions and state variables without explicit visibility
- Missing pragmas to for compiler version
- Race conditions, such as contract Reentrancy
- Transaction front running
- Timestamp dependence
- An integer overflow and underflow
- Code blocks that consume a non-constant amount of gas, that grows over block gas limit.
- Denial of Service attacks
- Suspicious code or underhanded code.
- Error-prone code constructs
- Race conditions
Based on these criteria it is possible to evaluate some really useful tools you can use to test your smart contract. Let’s see which ones our team has decided to use.
What Are the Tools?
Reliable smart contacts can’t be written without proper testing. In order to make sure your code has no vulnerabilities and is reliable, you’ll need to use special tools that help detect all the issues. We’ve gone through a number of “bug-hunters” and now share the top of our shortlist:
- Smart Contracts code checker: for bad practices and vulnerabilities
- Mythril: a security analysis tool for Ethereum smart contract
- Visualize Solidity control flow for smart contract security analysis
In this article, we share the test cases for Smart Contracts code checker and Mythril security analysis tool.
Checking code by Mythril security tool
Integer Overflow
Type: Warning
Contract: ERC20FreezableImplementation
Function name: _function_transferBulk
PC address: 1224
A possible integer overflow exists in the function `_function_transferBulk`.
The addition or multiplication may result in a value higher than the maximum representable integer.
— — — — — — — — — —
function transferBulk(address[] _toAccounts, uint256[] _tokenAmount) onlyOwner public {
require(_toAccounts.length == _tokenAmount.length);
for(uint i=0; i<_toAccounts.length; i++) {
balances[msg.sender] = balances[msg.sender].sub(_tokenAmount[i]);
balances[_toAccounts[i]] = balances[_toAccounts[i]].add(_tokenAmount[i]);
if(!isInvestor[_toAccounts[i]]){
isInvestor[_toAccounts[i]] = true;
investors.push(_toAccounts[i]);
}
}
}
— — — — — — — — — —
Resolution: This overflow can be, when _toAccounts.length will be > 2**256+1. It is not possible because of block gas limit. Also, we are using safeMath library, which prevents all of this.
Exception state
Type: Informational
Contract: ERC20FreezableImplementation
Function name: _function_0xd73dd623
PC address: 9071
A reachable exception has been detected. This can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. This is acceptable in most situations. Note however that `assert()` should only be used to check invariants. Use `require()` for regular input checking.
— — — — — — — — — —
In file: contracts/SafeMath.sol:45
assert(c >= a)
— — — — — — — — — —
Resolution: Just information that exception could be. It’s expected behavior.
Exception state
Type: Informational
Contract: ERC20FreezableImplementation
Function name: _function_0x9316c3e7
PC address: 9040
A reachable exception (opcode 0xfe) has been detected. This can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. This is acceptable in most situations. Note however that `assert()` should only be used to check invariants. Use `require()` for regular input checking.
— — — — — — — — — —
In file: contracts/SafeMath.sol:36
assert(b <= a)
— — — — — — — — — —
Resolution: Just information that exception could be. It’s expected behavior.
Exception state
Type: Informational
Contract: ERC20FreezableImplementation
Function name: _function_0xd880ae89
PC address: 8396
A reachable exception (opcode 0xfe) has been detected. This can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. This is acceptable in most situations. Note however that `assert()` should only be used to check invariants. Use `require()` for regular input checking.
— — — — — — — — — —
investors[i]
— — — — — — — — — —
==== Integer Overflow ====
Type: Warning
Contract: ERC20FreezableImplementation
Function name: _function_0xb2f5a54c
PC address: 7009
A possible integer overflow exists in the function `_function_0xb2f5a54c`.
The addition or multiplication may result in a value higher than the maximum representable integer.
— — — — — — — — — —
return investors
— — — — — — — — — —
Checking code by SmartCheck security tool
Reentrancy
Any interaction from a contract (A) with another contract (B) and any transfer of Ether hands over control to that contract (B). This makes it possible for B to call back into A before this interaction is completed.
This pattern is experimental and can report false issues. This pattern might also be triggered when
- accessing the structure’s field;
- using enumeration element.
To avoid reentrancy, you can use the Checks-Effects-Interactions pattern as outlined further below:
pragma solidity ^0.4.11;
contract Fund {
/// Mapping of ether shares of the contract.
mapping(address => uint) shares;
/// Withdraw your share.
function withdraw() {
var share = shares[msg.sender];
shares[msg.sender] = 0;
msg.sender.transfer(share);
}
}
Note that reentrancy is not only an effect of Ether transfer but of any function call on another contract. Furthermore, you also have to take multi-contract situations into account. A called contract could modify the state of another contract you depend on.
Resolution: We avoid this problem with a line like:
balances[_to] = balances[_to].add(_value);
In the mining moment of the transaction, we are getting the actual value of the balance in the current moment.
Compiler version not fixed
Solidity source files indicate the versions of the compiler they can be compiled with.
pragma solidity ^0.4.17; // bad: compiles w 0.4.17 and above
pragma solidity 0.4.17; // good : compiles w 0.4.17 only
Resolution: It is recommended to follow the latter example, as future compiler versions may handle certain language constructions in a way the developer did not foresee.
No payable fallback function
The contract does not have a payable fallback. All attempts to transfer or send ether to this contract will be reverted.
Resolution: This is an expected behavior.
Integration testing
We can’t make full integration testing with ganache-cli because it’s a bug with using call() method in web3js. It doesn’t allow call function which is not using gas with gas limit > then block size limit.
Resolution: So we need to launch our personal node of the test network for full integration testing.
Tests which should be made:
1) Check TransferBack method (This method is used for manually solving possible transfer errors. It transfers tokens from a given address back to the token owner) with different scenarios:
- When address of transaction sender is not owner ⇒ should fail;
- When address parameter is “0x0000000000000000000000000000000000000000” ⇒ should fail;
- When value > account.balance ⇒ should fail;
- When owner calls these methods with real parameters and address balance >= value ⇒ should pass;
2) Check TransferBulk (Transfer tokens from owner to array of addresses) method with different scenarios:
- When array of recipients and array of token amounts for recipients are different ⇒ should fail;
- When address of transaction sender is not owner ⇒ should fail;
- When owner calls these methods with real parameters ⇒ should pass;
3) Check getInvestors method:
- Should return array with all investors;
4) Check getInvestorsBalances method:
- Should return array with investor balances without any gas usage;
5) Check freeze() method:
- When address of transaction sender is not owner ⇒ should fail;
- Should lock all transfer methods except transferBack and transferBulk, when user tries to call transfer method, should be revert;
6) Check unFreeze() method:
- When address of transaction sender is not owner ⇒ should fail;
- Should unlock all transfer methods which was locked by freeze() method;
ERC20 Testing Cases
In order to extend your understanding of smart contracts testing, we’ve run a number of tests for ERC20. Yes, we know a number of tests had already been done and there’s no reason to run tests ourselves. But there’s only one team we can trust in terms of high-quality development and security, so we did them on our own. You can find a list of test cases we went through using the tools described above.
Hooray! We congratulate you for having learned the basics of smart contracts testing. Now you’ll definitely be ahead of your competitors in terms of safety and reliability.
Have some questions about smart contracts or got a mind-blowing project idea? Just drop us a line and speak with one of our Blockchain experts!
Originally published at zfort.com