Smart Contract Security
Contents
Security Best Practices
Defensive programming is a style of programming that is particularly well suited to smart contracts. It emphasizes the following best practices:
- Minimalism/simplicity
- Code reuse
- Code quality
- Readability/auditability
- Smart contracts are public, as everyone can read the bytecode and anyone can reverse-engineer it.
- Develop your work in public, using collaborative and open source methodologies, to draw upon the collective wisdom of the developer community and benefit from the highest common denominator of open source development.
- Write code that is well documented and easy to read, following the style and naming conventions that are part of the Ethereum community.
- Test coverage
Security Risks and Antipatterns
Reentrancy
This type of attack can occur when a contract sends ether to an unknown address.
- An attacker can carefully construct a contract at an external address that contains malicious code in the fallback function. Thus, when a contract sends ether to this address, it will invoke the malicious code. Typically the malicious code executes a function on the vulnerable contract.
- The term “reentrancy” comes from the fact that the external malicious contract calls a function on the vulnerable contract and the path of code execution “reenters” it.
Demonstration:
// EtherStore.sol
contract EtherStore {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
require(_weiToWithdraw <= withdrawalLimit); // limit the withdrawal
require(now >= lastWithdrawTime[msg.sender] + 1 weeks); // limit the time allowed to withdraw
require(msg.sender.call.value(_weiToWithdraw)()); // !!! vulnerability
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
}
}
// Attack.sol
import "EtherStore.sol";
contract Attack {
EtherStore public etherStore;
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
function attackEtherStore() public payable {
// attack to the nearest ether
require(msg.value >= 1 ether);
etherStore.depositFunds.value(1 ether)();
etherStore.withdrawFunds(1 ether); // start the magic
}
function collectEther() public {
msg.sender.transfer(this.balance);
}
// fallback function - where the magic happens
function () payable {
if (etherStore.balance > 1 ether) {
etherStore.withdrawFunds(1 ether);
}
}
}
- The attacker created the malicious contract with the
EtherStore
’s contract address0x0...123
as the sole constructor parameter.- Assume the current balance of
EtherStore
contract is10 ether
.
- Assume the current balance of
- The attacker called the
attackEtherStore
with amount of 1 ether and a lot of gas. etherStore.depositFunds.value(1 ether)();
:balances[0x0..123] = 1 ether
.etherStore.withdrawFunds(1 ether);
EtherStore
contract sent 1 ether back to the malicious contract.- The payment to the malicious contract executed the fallback function.
- The total balance of the
EtherStore
contract was10 ether
and is now9 ether
, so thisif
statement passes. - The fallback function calls the
EtherStore
withdrawFunds
function again and reenters theEtherStore
contract. - In this second call to
withdrawFunds
, the attacking contract’s balance is still 1 ether asbalances[msg.sender] -= _weiToWithdraw;
has not yet been executed. Thus, we still havebalances[0x0..123] = 1 ether
. This is also the case for thelastWithdraw
Time variable. Again, we pass all the requirements. - The attacking contract withdraws another
1 ether
. - Steps 6–10 repeat until it is no longer the case that
EtherStore.balance > 1
. - Once there is 1 (or less) ether left in the
EtherStore
contract, thisif
statement will fail. This will then allowbalances[msg.sender] -= _weiToWithdraw;
andlastWithdrawTime[msg.sender] = now;
of theEtherStore
contract to be executed (for each call to thewithdrawFunds
function).
The final result is that the attacker has withdrawn all but 1 ether from the EtherStore
contract in a single transaction.
Preventative techniques:
- Whenever possible use the built-in
transfer
function (instead of lower-level alternatives) when sending ether to external contracts.- The
transfer
function only sends 2300 gas with the external call, which is not enough for the destination address/contract to call another contract (i.e., reenter the sending contract).
- The
- Ensure that all logic that changes state variables happens before ether is sent out of the contract (or any external call).
- It is good practice for any code that performs external calls to unknown addresses to be the last operation in a localized function or piece of code execution (known as the checks-effects-interactions pattern ).
- Introduce a mutex: add a state variable that locks the contract during code execution.
Arithmetic Over/Underflows
An over/underflow occurs when an operation is performed that requires a fixed-size variable to store a number (or piece of data) that is outside the range of the variable’s data type.
- underflow:
- Subtracting 1 from a
uint8
variable whose value is0
will result in the number255
. - Adding
2^8=256
to auint8
will leave the variable unchanged, as we have wrapped around the entire length of theuint
.
- Subtracting 1 from a
- overflow:
- Adding numbers larger than the data type’s range is called an overflow.
- Adding
257
to auint8
that currently has a value of0
will result in the number1
.
It is sometimes instructive to think of fixed-size variables as being cyclic.
Demonstration:
function deposit() public payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = now + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease; // overflow
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
balances[msg.sender] = 0;
msg.sender.transfer(balance);
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0); // underflow
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
Preventative techniques:
- Use or build mathematical libraries that replace the standard math operators addition, subtraction, and multiplication (division is excluded as it does not cause over/underflows and the EVM reverts on division by 0).
Unexpected Ether
Typically, when ether is sent to a contract it must execute either the fallback function or another function defined in the contract.
There are two ways in which ether can (forcibly) be sent to a contract without using a payable function or executing any code on the contract:
- Self-destruct/suicide (
selfdestruct(address payable recipient)
)- If this specified address is also a contract, no functions (including the fallback) get called.
- The
selfdestruct
function can be used to forcibly send ether to any contract regardless of any code that may exist in the contract, even contracts with no payable functions. - self-destruct opcode (Quirk #2)
- Pre-sent ether
- Contract addresses are deterministic, calculated from the Keccak-256 hash of the address creating the contract and the transaction nonce that creates the contract. This means anyone can calculate what a contract’s address will be before it is created and send ether to that address. When the contract is created it will have a nonzero ether balance.
address = sha3(rlp.encode([account_address, transaction_nonce]))
- Keyless Ether
- Contract addresses are deterministic, calculated from the Keccak-256 hash of the address creating the contract and the transaction nonce that creates the contract. This means anyone can calculate what a contract’s address will be before it is created and send ether to that address. When the contract is created it will have a nonzero ether balance.
The smoking gun for this vulnerability is the (incorrect) use of this.balance
.
Contract logic, when possible, should avoid being dependent on exact values of the balance of the contract, because it can be artificially manipulated.
Preventative techniques:
- If exact values of deposited ether are required, a self-defined variable should be used that is incremented in payable functions, to safely track the deposited ether.
DELEGATECALL
The CALL
and DELEGATECALL
opcodes are useful in allowing Ethereum developers to modularize their code, enabling the implementation of libraries, allowing developers to deploy reusable code once and call it from future contracts.
- Standard external message calls to contracts are handled by the
CALL
opcode, whereby code is run in the context of the external contract/function. - The
DELEGATECALL
opcode is almost identical, except that the code executed at the targeted address is run in the context of the calling contract, andmsg.sender
andmsg.value
remain unchanged.
The use of DELEGATECALL
can lead to unexpected code execution.
- The code in libraries themselves can be secure and vulnerability-free; however, when run in the context of another application new vulnerabilities can arise.
delegatecall
preserves contract context, which means that code that is executed via delegatecall
will act on the state (i.e., storage) of the calling contract.
- When we say that
delegatecall
is state-preserving, we are not talking about the variable names of the contract, but rather the actual storage slots to which those names point. - This makes it possible to implement reusable library code that can be applied to a contract’s storage.
Demonstration:
contract FibonacciLib {
uint public start; // slot[0]
uint public calculatedFibNumber; // slot[1]
function setStart(uint _start) public {
start = _start;
}
function setFibonacci(uint n) public {
calculatedFibNumber = fibonacci(n);
}
function fibonacci(uint n) internal returns (uint) {
if (n == 0) return start;
else if (n == 1) return start + 1;
else return fibonacci(n - 1) + fibonacci(n - 2);
}
}
contract FibonacciBalance {
address public fibonacciLibrary; // slot[0]
// the current Fibonacci number to withdraw
uint public calculatedFibNumber; // slot[1]
// the starting Fibonacci sequence number
uint public start = 3;
uint public withdrawalCounter;
bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)")); // the Fibonancci function selector
// constructor - loads the contract with ether
constructor(address _fibonacciLibrary) public payable {
fibonacciLibrary = _fibonacciLibrary;
}
function withdraw() {
withdrawalCounter += 1;
// calculate the Fibonacci number for the current withdrawal user-
// this sets calculatedFibNumber
require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
msg.sender.transfer(calculatedFibNumber * 1 ether);
}
// allow users to call Fibonacci library functions
function() public {
require(fibonacciLibrary.delegatecall(msg.data));
}
}
-
fibonacci(n)
references start (slot[0]
), which in the current calling context is thefibonacciLibrary
address (which will often be quite large, when interpreted as auint
). Thus it is likely that thewithdraw
function will revert, as it will not containuint(fibonacciLibrary)
amount of ether, which is whatcalculatedFibNumber
will return.fibonacciLibrary.delegatecall(fibSig, withdrawalCounter)
modifies storageslot[1]
, which in current context iscalculatedFibNumber
(same as that inFibonacciLib
). This is as expected.
-
The
FibonacciBalance
contract allows users to call all of thefibonacciLibrary
functions via the fallback function. This includes thesetStart
function, which allows anyone to modify or set storageslot[0]
inFibonacciBalance
context.- An attacker could create a malicious contract, convert the address to a
uint
, and then callsetStart(<attack_contract_address_as_uint>)
.
contract Attack { uint storageSlot0; // corresponds to fibonacciLibrary uint storageSlot1; // corresponds to calculatedFibNumber // fallback - this will run if a specified function is not found function() public { storageSlot1 = 0; // we set calculatedFibNumber to 0, so if withdraw is called we don't send out any ether <attacker_address>.transfer(this.balance); // we take all the ether } }
- An attacker could create a malicious contract, convert the address to a
Solidity provides the library
keyword for implementing library contracts. This ensures the library contract is stateless and non-self-destructable. Forcing libraries to be stateless mitigates the complexities of storage context. Stateless libraries also prevent attacks wherein attackers modify the state of the library directly in order to affect the contracts that depend on the library’s code.
Preventative techniques:
- As a general rule of thumb, when using
DELEGATECALL
pay careful attention to the possible calling context of both the library contract and the calling contract, and whenever possible build stateless libraries.
Default Visibilities
The default visibility for functions is public
, so functions that do not specify their visibility will be callable by external users. The issue arises when developers mistakenly omit visibility specifiers on functions that should be private (or only callable within the contract itself).
Preventative techniques:
- Always specify the visibility of all functions in a contract, even if they are intentionally
public
.
Entropy Illusion
All transactions on the Ethereum blockchain are deterministic state transition operations.
- This means that every transaction modifies the global state of the Ethereum ecosystem in a calculable way, with no uncertainty.
- This has the fundamental implication that there is no source of entropy or randomness in Ethereum.
Solutions to achieving decentralized:
Uncertainty must come from a source external to the blockchain.
- This can be done among peers with systems such as commit–reveal , or via changing the trust model to a group of participants (as in RANDAO).
Some of the first contracts built on the Ethereum platform were based around gambling. Fundamentally, gambling requires uncertainty (something to bet on).
- A common pitfall is to use future block variables—that is, variables containing information about the transaction block whose values are not yet known, such as hashes, timestamps, block numbers, or gas limits.
- The issue with these are that they are controlled by the miner who mines the block, and as such are not truly random.
- Using solely block variables means that the pseudorandom number will be the same for all transactions in a block, so an attacker can multiply their wins by doing many transactions within a block (should there be a maximum bet).
- Using past or present variables can be even more devastating: An Ethereum Roulette
Preventative techniques:
- The source of entropy (randomness) must be external to the blockchain.
- Block variables (in general, there are some exceptions) should not be used to source entropy, as they can be manipulated by miners.
External Contract Referencing
A large number of contracts reference external contracts, usually via external message calls, to reuse code and interact with contracts already deployed on the network. These external message calls can mask malicious actors’ intentions in some nonobvious ways.
In Solidity, any address can be cast to a contract, regardless of whether the code at the address represents the contract type being cast.
A privileged user can change the address of referenced contract, getting other users to unknowingly run arbitrary code.
- Even safe contracts can in some cases be deployed in such a way that they behave maliciously. An auditor could publicly verify a contract and have its owner deploy it in a malicious way, resulting in a publicly audited contract that has vulnerabilities or malicious intent.
Preventative techniques:
- Use the
new
keyword to create contracts at deployment time. - Hardcode external contract addresses.
As a developer, when defining external contracts, it can be a good idea to make the contract addresses public to allow users to easily examine code referenced by the contract.
A contract has a private variable contract address it can be a sign of someone behaving maliciously.
- If a user can change a contract address that is used to call external functions, it can be important (in a decentralized system context) to implement a time-lock and/or voting mechanism to allow users to see what code is being changed, or to give participants a chance to opt in/out with the new contract address.
Short Address/Parameter Attack
This attack is not performed on Solidity contracts themselves, but on third-party applications that may interact with them.
-
When passing parameters to a smart contract, the parameters are encoded according to the ABI specification.
-
It is possible to send encoded parameters that are shorter than the expected parameter length (for example, sending an address that is only 38 hex chars (19 bytes) instead of the standard 40 hex chars (20 bytes)). In such a scenario, the EVM will add zeros to the end of the encoded parameters to make up the expected length.
- This becomes an issue when third-party applications do not validate inputs.
function transfer(address to, uint tokens) public returns (bool success); a9059cbb000000000000000000000000deaddeaddea \ ddeaddeaddeaddeaddeaddeaddead0000000000000 000000000000000000000000000000000056bc75e2d63100000 // address misses 1 byte (2 hex digits): ad; 00 was added to the end of the encoding, value being 25600 tokens a9059cbb000000000000000000000000deaddeaddea \ ddeaddeaddeaddeaddeaddeadde00000000000000 00000000000000000000000000000000056bc75e2d6310000000
- The first 4 bytes (
a9059cbb
) are thetransfer
function signature/selector, the next 32 bytes are the address, and the final 32 bytes represent theuint256
number of tokens (100 tokens with 18 decimal places). - If the exchange held this many tokens, the user would withdraw 25600 tokens (while the exchange thinks the user is only withdrawing 100) to the modified address.
- The attacker won’t possess the modified address in this example, but if the attacker were to generate any address that ended in 0s (which can be easily brute-forced) and used this generated address, they could steal tokens from the unsuspecting exchange.
Preventative techniques:
- All input parameters in external applications should be validated before sending them to the blockchain.
- Parameter ordering plays an important role here. As padding only occurs at the end, careful ordering of parameters in the smart contract can mitigate some forms of this attack.
Unchecked CALL Return Values
There are a number of ways of performing external calls like sending ether to external accounts:
- It is commonly performed via the
transfer
method. - The
send
function can also be used. - For more versatile external calls the
CALL
opcode can be directly employed.
The call
and send
functions return a Boolean indicating whether the call succeeded or failed.
- These functions have a simple caveat, in that the transaction that executes these functions will not revert if the external call (intialized by
call
orsend
) fails; rather, the functions will simply returnfalse
. - A common error is that the developer expects a revert to occur if the external call fails, and does not check the return value.
Preventative techniques:
- Whenever possible, use the
transfer
function rather thansend
, astransfer
will revert if the external transaction reverts. Ifsend
is required, always check the return value. - Adopt a withdrawal pattern: each user must call an isolated
withdraw
function that handles the sending of ether out of the contract and deals with the consequences of failed send transactions.- The idea is to logically isolate the external send functionality from the rest of the codebase, and place the burden of a potentially failed transaction on the end user calling the
withdraw
function.
- The idea is to logically isolate the external send functionality from the rest of the codebase, and place the burden of a potentially failed transaction on the end user calling the
Race Conditions/Front Running
The combination of external calls to other contracts and the multiuser nature of the underlying blockchain gives rise to a variety of potential Solidity pitfalls whereby users race code execution to obtain unexpected states.
- Reentrancy is one example of such a race condition.
Demonstration:
contract FindThisHash {
bytes32 constant public hash =
0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a;
constructor() public payable {} // load with ether
function solve(string solution) public {
// If you can find the pre-image of the hash, receive 1000 ether
require(hash == sha3(solution));
msg.sender.transfer(1000 ether);
}
}
- An attacker watches the transaction pool for transactions that may contain solutions to this problem. He sees this solution, check its validity, and then submit an equivalent transaction with a much higher
gasPrice
than the original transaction.- The miner who solves the block will likely give the attacker preference due to the higher
gasPrice
, and mine his transaction before the original solver’s. The attacker will take the 1,000 ether, and the user who solved the problem will get nothing.
- The miner who solves the block will likely give the attacker preference due to the higher
The ERC20 standard has a potential front-running vulnerability that comes about due to the approve
function.
function approve(address _spender, uint256 _value) returns (bool success)
- This function allows a user to permit other users to transfer tokens on their behalf.
- Front-running vulnerability scenario:
- A user Alice approves her friend Bob to spend 100 tokens.
- Alice later decides that she wants to revoke Bob’s approval to spend, say, 100 tokens, so she creates a transaction that sets Bob’s allocation to 50 tokens.
- Bob, who has been carefully watching the chain, sees this transaction and builds a transaction of his own spending the 100 tokens. He puts a higher
gasPrice
on his transaction than Alice’s, so gets his transaction prioritized over hers. - Some implementations of
approve
would allow Bob to transfer his 100 tokens and then, when Alice’s transaction is committed, reset Bob’s approval to 50 tokens, in effect giving Bob access to 150 tokens.
Real-world example: Bancor
- If you see a large BUY is about to happen, you know the BNT price will increase (following their deterministic formula), so if you buy in before that transaction you get an instant appreciation of your tokens and a guaranteed return on your investment.
- Similarly, if somebody sent out a pending SELL, an attacker can sell their tokens in front.
There are two classes of actor who can perform these kinds of front-running attacks: users (who modify the gasPrice
of their transactions) and miners themselves (who can reorder the transactions in a block how they see fit).
Preventative techniques:
- One method is to place an upper bound on the
gasPrice
.- This prevents users from increasing the
gasPrice
and getting preferential transaction ordering beyond the upper bound. - This measure only guards against the first class of attackers. Miners in this scenario can still attack the contract, as they can order the transactions in their block however they like, regardless of gas price.
- This prevents users from increasing the
- Use a commit–reveal scheme.
- Users send transactions with hidden information (typically a hash). After the transaction has been included in a block, the user sends a transaction revealing the data that was sent (the reveal phase).
- This method prevents both miners and users from frontrunning transactions, as they cannot determine the contents of the transaction.
- This method, however, cannot conceal the transaction value, which in some cases is the valuable information that needs to be hidden.
- The ENS smart contract allowed users to send transactions whose committed data included the amount of ether they were willing to spend. Users could then send transactions of arbitrary value. During the reveal phase, users were refunded the difference between the amount sent in the transaction and the amount they were willing to spend.
Denial of Service (DoS)
A few less-obvious Solidity coding patterns that can lead to DoS vulnerabilities:
-
Looping through externally manipulated mappings or arrays
function distribute() public { require(msg.sender == owner); // only owner for(uint i = 0; i < investors.length; i++) { // transfers "amount" of tokens to the address "to" transferToken(investors[i], investorTokens[i]); } }
- An attacker can create many user accounts, making the
investor
array large. - In principle this can be done such that the gas required to execute the for loop exceeds the block gas limit, essentially making the
distribute
function inoperable.
- An attacker can create many user accounts, making the
-
Owner operations
- If the privileged user loses their private keys or becomes inactive, the entire contract becomes inoperable.
-
Progressing state based on external calls
- Contracts are sometimes written such that progressing to a new state requires sending ether to an address, or waiting for some input from an external source. These patterns can lead to DoS attacks when the external call fails or is prevented for external reasons.
- In the example of sending ether, a user can create a contract that does not accept ether.
- If a contract requires ether to be withdrawn in order to progress to a new state (consider a time-locking contract that requires all ether to be withdrawn before being usable again), the contract will never achieve the new state, as ether can never be sent to the user’s contract that does not accept ether.
- Contracts are sometimes written such that progressing to a new state requires sending ether to an address, or waiting for some input from an external source. These patterns can lead to DoS attacks when the external call fails or is prevented for external reasons.
Preventative techniques:
- In the first example, contracts should not loop through data structures that can be artificially manipulated by external users. A withdrawal pattern is recommended, whereby each of the investors call a
withdraw
function to claim tokens independently. - In the second example, a failsafe can be used in the event that the owner becomes incapacitated.
- One solution is to make the owner a multisig contract.
- Another solution is to use a time-lock.
- This solution can also be used in the third example.
Block Timestamp Manipulation
Block timestamps have historically been used for a variety of applications, such as entropy for random numbers, locking funds for periods of time, and various state-changing conditional statements that are time-dependent.
Miners have the ability to adjust timestamps slightly, which can prove to be dangerous if block timestamps are used incorrectly in smart contracts.
block.timestamp
and its aliasnow
can be manipulated by miners if they have some incentive to do so.- In practice, block timestamps are monotonically increasing and so miners cannot choose arbitrary block timestamps (they must be later than their predecessors). They are also limited to setting block times not too far in the future, as these blocks will likely be rejected by the network (nodes will not validate blocks whose timestamps are in the future).
Preventative techniques:
- Block timestamps should not be used for entropy or generating random numbers—i.e., they should not be the deciding factor (either directly or through some derivation) for winning a game or changing an important state.
- It is sometimes recommended to use
block.number
and an average block time to estimate times; with a10 second
block time,1 week
equates to approximately, 60480 blocks. Thus, specifying a block number at which to change a contract state can be more secure, as miners are unable easily to manipulate the block number.
Uninitialized Storage Pointers
The EVM stores data either as storage or as memory.
- Local variables within functions default to storage or memory depending on their type.
- Uninitialized local storage variables may contain the value of other storage variables in the contract; this fact can cause unintentional vulnerabilities, or be exploited deliberately.
Demonstration:
contract NameRegistrar {
bool public unlocked = false; // slot[0]; registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name;
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // slot[1]; records who registered names; map the name to an address
mapping(bytes32 => address) public resolve; // slot[2]; resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}
- The Boolean unlocked will look like
0x000...0
(64 0s, excluding the0x
) forfalse
or0x000...1
(630
s) fortrue
. - Solidity by default puts complex data types, such as structs, in storage when initializing them as local variables.
- Because it defaults to storage, it is mapped to storage
slot[0]
, which currently contains a pointer tounlocked
. newRecord.name = _name;
andnewRecord.mappedAddress = _mappedAddress;
then setnewRecord.name
to_name
andnewRecord.mappedAddress
to_mappedAddress
.- This updates the storage locations of
slot[0]
andslot[1]
, which modifies bothunlocked
and the storage slot associated withregisteredNameRecord
.
- This updates the storage locations of
- This means that
unlocked
can be directly modified, simply by thebytes32 _name
parameter of theregister
function.- If the last byte of
_name
is nonzero, it will modify the last byte of storageslot[0]
and directly changeunlocked
totrue
. Such_name
values will cause the require callrequire(unlocked);
to succeed, as we have setunlocked
totrue
.
- If the last byte of
- Because it defaults to storage, it is mapped to storage
Preventative techniques:
- The Solidity compiler shows a warning for unintialized storage variables.
- Explicitly use the
memory
orstorage
specifiers when dealing with complex types, to ensure they behave as expected.
Floating Point and Precision
Solidity does not support fixed-point and floating-point numbers as of v0.8.17. This means that floating-point representations must be constructed with integer types.
Best practices:
- Ensure that any ratios or rates you are using allow for large numerators in fractions.
- Be mindful of order of operations.
- Convert values to higher precision, perform all mathematical operations, then finally convert back down to the precision required for output.
- Typically
uint256
s are used (as they are optimal for gas usage); these give approximately 60 orders of magnitude in their range, some of which can be dedicated to the precision of mathematical operations. - It may be the case that it is better to keep all variables in high precision in Solidity and convert back to lower precisions in external apps (this is essentially how the
decimals
variable works in ERC20 token contracts).
- Typically
tx.Origin
Authentication
Solidity has a global variable, tx.origin
, which traverses the entire call stack and contains the address of the account that originally sent the call (or transaction).
Contracts that authorize users using the tx.origin
variable are typically vulnerable to phishing attacks that can trick users into performing authenticated actions on the vulnerable contract.
Preventative techniques:
tx.origin
should not be used for authorization in smart contracts.require(tx.origin == msg.sender)
can be used to prevents intermediate contracts being used to call the current contract, limiting the contract to regular codeless addresses.
References
- Mastering Ethereum by Andreas M. Antonopoulos and Dr. Gavin Wood (O’Reilly). Copyright 2019 The Ethereum Book LLC and Gavin Wood, 978-1-491-97194-9