In Ethereum, transactions cost gas and hence ether. The gas consumption of a transaction depends on the opcodes that the EVM has to execute. The gas cost for each Opcode can be found as explained in this question. Few common opcodes and gas are,
Operation Gas Description
ADD/SUB 3 Arithmetic operation
MUL/DIV 5 Arithmetic operation
ADDMOD/MULMOD 8 Arithmetic operation
AND/OR/XOR 3 Bitwise logic operation
LT/GT/SLT/SGT/EQ 3 Comparison operation
POP 2 Stack operation
PUSH/DUP/SWAP 3 Stack operation
MLOAD/MSTORE 3 Memory operation
JUMP 8 Unconditional jump
JUMPI 10 Conditional jump
SLOAD 200 Storage operation
SSTORE 5,000/20,000 Storage operation
BALANCE 400 Get balance of an account
CREATE 32,000 Create a new account using CREATE
CALL 25,000 Create a new account using CALL
This is a concern when it comes to smart contracts as transactions are also involved and it's important to consider the gas cost when designing a contract.
Reducing the gas consumed by a contract is important in two situations,
- Cost of deploying a contract
- Cost to call the contract functions
Cost of deploying a contract
For this, most of the optimizations are done at compilation time as described in the documentation-faqs,
Are comments included with deployed contracts and do they increase
deployment gas?
No, everything that is not needed for execution is
removed during compilation. This includes, among others, comments,
variable names and type names.
And the details of the optimizer can be found here.
Another way of reducing the size is by removing useless code. For example:
1 function p1 ( uint x ){
2 if ( x > 5)
3 if ( x*x < 20)
4 XXX }
In above code, line 3 and 4 will never be executed and these type of useless code can be avoided by carefully going through the contract logic and that will reduce the size of the smart contract.
Cost to call the contract functions
When contracts' functions are called, for the execution of function it costs gas. Hence optimizing functions to use less gas is important. There can be many different ways of doing it when individual contract is considered. Here are few that might save gas during execution,
- Reduce Expensive operations
Expensive operations are the opcodes that has more gas values such as SSTORE
. Below are some methods of reducing expensive operations.
A) Use of Short Circuiting rules
The operators || and && apply the common short-circuiting rules. This
means that in the expression f(x) || g(y), if f(x) evaluates to true,
g(y) will not be evaluated even if it may have side-effects.
So if a logical operation includes an expensive operation and a low cost operation, arranging in a way that the expensive operation can be short circuited will reduce gas at some executions.
If f(x) is low cost and g(y) is expensive, arranging logical operations
- OR :
f(x) || g(y)
- AND :
f(x) && g(y)
will save more gas if short circuited.
If f(x)
has a considerably higher probability of returning false compared to g(y)
, arranging AND operations as f(x) && g(y)
might cause to save more gas in execution by short circuiting.
If f(x)
has a considerably higher probability of returning true compared to g(y)
, arranging OR operations as f(x) || g(y)
might cause to save more gas in execution by short circuiting.
B) expensive operations in a loop
eg:
uint sum = 0;
function p3 ( uint x ){
for ( uint i = 0 ; i < x ; i++)
sum += i; }
In the above code, since sum
storage variable is read and written every time inside the loop, storage operations that are expensive take place at every iteration. This can be avoided by introducing a local variable as follows to save gas.
uint sum = 0;
function p3 ( uint x ){
uint temp = 0;
for ( uint i = 0 ; i < x ; i++)
temp += i; }
sum += temp;
- Other loop related patterns
loop combining
function p5 ( uint x ){
uint m = 0;
uint v = 0;
for ( uint i = 0 ; i < x ; i++) //loop-1
m += i;
for ( uint j = 0 ; j < x ; j++) //loop-2
v -= j; }
loop-1 and loop-2 can be combined and gas can be saved,
function p5 ( uint x ){
uint m = 0;
uint v = 0;
for ( uint i = 0 ; i < x ; i++) //loop-1
m += i;
v -= j; }
and a few more loop patterns can be found here.
- Using of Fixed-size bytes arrays
From Docs,
It is possible to use an array of bytes as byte[], but it is wasting a
lot of space, 31 bytes every element, to be exact, when passing in
calls. It is better to use bytes.
and
As a rule of thumb, use bytes for arbitrary-length raw byte data and
string for arbitrary-length string (UTF-8) data. If you can limit the
length to a certain number of bytes, always use one of bytes1 to
bytes32 because they are much cheaper.
having a fixed length always saves gas. Refer to this question as well.
Removing useless code as explained earlier under contract deployment will save gas even when functions are executed, if that can be done inside functions.
Not using libraries when implementing the functionality is cheaper for simple usages.
Calling library for simple usages may be costly. If the functionality is simple and feasible to implement inside the contract as it avoids the step of calling the library. execution cost for the functionality only will still be the same for both.
Using visibility external
for the functions only accessed externally forces to use calldata
as the parameter location and this saves some gas when the function executes.
Using memory
variables within functions locally when possible saves gas of accessing the storage
.
These are some ways of saving gas and there may be many other methods depending on the requirements.
Best Answer
This is the community wiki (no reputation) answer for possible attacks and how to protect against them. Feel free to update the list. If your contract functions have characteristics matching prerequisites carefully evaluate your function against the given advice.
This is the list of potential attacks or mispractices enabling those attacks only. For additional resources for smart contract programming best practices see the Resources link at the end of the answer.
Study potential attack vectors and history of past exploits
Those who do not learn history are doomed to repeat it. Here is a nice summary of known smart contract attacks.
Have more than one developer
One developer writes the code and the other reviews it. Having more than one set of eyeballs is important during development. Issues should be caught during the development time through public discussion, not in the audit.
Use well-known libraries
Do not try to develop smart contracts like ERC-20 yourself. Instead, use open source libraries that provide ready-made and battle-tested smart contracts. It is likely that when you develop something from the scratch you are going to make a mistake.
Gold standard libraries include, but are not limited to
Have a test suite
Try to ensure that your Solidity code has 100% code coverage with an automated test suite. This ensures your code is testable. The automatic test suite covers and runs every line and branch of the smart contract code at least once.
Solidity unit tests are usually written in Python (Brownie, web3.py) or JavaScript (Hardhat, Truffle).
Tests will do transactions against the smart contract and check that the state of the transaction is as indented post-transaction. (Pendatically, the state is always as the letter of the contract, however in this case the letter and the indent would not match.)
Test for positive cases and negative cases - i.e. things that should not happen even though you know that it does not happen. Sometimes, when the code lives and is updated, some new issues slip through and they would be caught by the past tests - this is called regression testing.
Doing audits without tests is not very productive, as the tests should be always the first line of defence what comes to write robust code.
Set up Github continuous integration that executes all the tests for everyone commit. Reports are automatically stored for the future. This also helps other people to replicate the test environment and run it later, as often due to package upgrades the test runner tools themselves start to fail.
Example
Correct use of function visibility modifiers
Internal functions are marked as such and only the proper author can call the function.
Please see The Parity Wallet Hack Explained.
Call stack attack
Synonyms: Shallow stack attack, stack attack
Prerequisites: Functions uses
send()
orcall()
Invoking: The attacker manipulates cross-contract call stack to call() to fail by calling contract with stack of 1023.
Protection: Always check return value of a send() and call(). Prefer
someAddress.send()
oversomeAddress.call.value()
More info
Re-entrancy attack
Synonyms: Race condition
Prerequisites: Functions uses
send()
orcall()
for ethers, ortransferFrom()
for ERC-20 tokens orsend()
for ERC-777 tokens.Invoking: The untrusted called contract calls the same function back, having it in unexpected state. This is how TheDAO was hacked.The attack can be chained over several of functions (cross function race condition).
Protection: Protect your functions with re-entrancy guards. Use Check-Effect-Interact order of actions in your functions that call anything that could be reflected back to the smartcontract.
Check-effects-interact
Use this pattern to minimize the damage of potential re-entry attack.
First Check, run things like
require()
Then Effect, update counters, like
balance[address] -= 10
Last, do anything that is Interact and will run code in other contracts through
send()
,call()
,transferFrom()
and others.More info
https://github.com/ConsenSys/smart-contract-best-practices
https://twitter.com/bneiluj/status/1251448415503908864
Does reentrancy attack happens as soon as the balance in storage is modified after the withdrawal?
DoS with unexpectd throw
Prerequisites: Functions uses
send()
orcall()
with throw following on failInvoking: The attacker manipulates the contract state so that
send()
always fails (e.g. refund)Protection: Prefer pull payment system over
send()
More info
Economic attacks
Prerequisies: Your smart contract reads price data and trades based on it
Invoking: Arbitration opportunites and unsafe price feeds may not be exploits per se, but still expose the users to the loss of funds that could have been otherwise avoided. In an economic attack, the attacker exploits the opportunity to trade for profit against someone, usually based on disparencies in the price of a token.
Protection: Do not rely on the spot price given by the exchanges or automated market-making smart contracts. All prices, including stablecoin prices, are subject to manipulation.
Popular automated market makers and smart contracts provide safe functions to calculate the price in different situations.
More info
Malicious libraries
Prerequisites: Using an external contract as a library and obtaining it through the registry.
Invoking: Call another contract function through a contract registry (see
library
keyword in Solidity).Protection: Ensure no dynamic parts which can be swapped out in future versions.
Integer overflow
Prerequisites: Function accepts an uint argument with is used in math
Invoking: Sending very big or very negative integer causing the sum calculation to overflow
Protection: Always check the order of values when doing math operations. E.g. https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
More info
Integer division round down
Prerequisites: Payment logic requires division operator /
Invoking: Programmer's error
Protection: Be aware that divisions are always rounded down
Loop length and gas manipulation
Others: Allocating too small int for arrays
Prerequisites: Any loop, copy arrays or strings inside the storage. A for loop where contract users can increase the length of the loop. Consider voting scenario loops.
Invoking: The attacker increases the array length or manipulates block gas limit
Protection: Use pull style payment systems. Spread
send()
over multiple transactions and checkmsg.gas
limit.https://blog.ethereum.org/2016/06/10/smart-contract-security/
https://github.com/ConsenSys/smart-contract-best-practices
https://ethereum.stackexchange.com/a/7298/620
Fallback function consuming more than the limit of 2300 gas
Prerequisites: A Solidity contract with catch all function() { } to receive generic sends
Invoking: Programmer's error
Protection: 100% test coverage. Make sure your fallback function stays below 2300 gas. Check for all branches of the function using test suite. Don't store anything in fallback function. Don't call contracts or send ethers in fallback function.
More info:
https://blog.ethereum.org/2016/06/10/smart-contract-security/
https://github.com/ConsenSys/smart-contract-best-practices
Forced balance update
Prerequisites: Function reads contract total balance and has some logic depending on it
Invoking: selfdestruct(contractaddress) can forcible upgrade its balance
Protection: Don't trust this.balance to stay within given limits
More
Miner frontrunning
Synonym: Transaction-Ordering Dependence (TOD)
Prerequisites: A bid style market, like DAI liqudation and auctions
Invoking: The attacker sees transactions in a mempool before they are finalized in blockchain. The attacker has a priviledged connection, like a mining pool, to broadcast his transaction first and override the original benefactor.
Protection: Pre-commit schemes
More
https://www.theblockcrypto.com/post/45750/exploring-defi-trading-strategies-arbitrage-in-defi
https://github.com/ConsenSys/smart-contract-best-practices
Static analysis tools
Static analysis tools that check the code for commonly known errors, like integer oveflows. They cannot check the intent of the code, but they run try to analyse the code against well known common problems.
Myhtril
Slither
Resources
OpenZeppelin checklist for things to do before a smart contract audit
https://github.com/ConsenSys/smart-contract-best-practices
https://blog.ethereum.org/2016/06/10/smart-contract-security/