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);
        }
    }
}
  1. The attacker created the malicious contract with the EtherStore’s contract address 0x0...123 as the sole constructor parameter.
    • Assume the current balance of EtherStore contract is 10 ether.
  2. The attacker called the attackEtherStore with amount of 1 ether and a lot of gas.
  3. etherStore.depositFunds.value(1 ether)();: balances[0x0..123] = 1 ether.
  4. etherStore.withdrawFunds(1 ether);
  5. EtherStore contract sent 1 ether back to the malicious contract.
  6. The payment to the malicious contract executed the fallback function.
  7. The total balance of the EtherStore contract was 10 ether and is now 9 ether, so this if statement passes.
  8. The fallback function calls the EtherStore withdrawFunds function again and reenters the EtherStore contract.
  9. In this second call to withdrawFunds, the attacking contract’s balance is still 1 ether as balances[msg.sender] -= _weiToWithdraw; has not yet been executed. Thus, we still have balances[0x0..123] = 1 ether. This is also the case for the lastWithdraw Time variable. Again, we pass all the requirements.
  10. The attacking contract withdraws another 1 ether.
  11. Steps 6–10 repeat until it is no longer the case that EtherStore.balance > 1.
  12. Once there is 1 (or less) ether left in the EtherStore contract, this if statement will fail. This will then allow balances[msg.sender] -= _weiToWithdraw; and lastWithdrawTime[msg.sender] = now; of the EtherStore contract to be executed (for each call to the withdrawFunds function).

The final result is that the attacker has withdrawn all but 1 ether from the EtherStore contract in a single transaction.

Preventative techniques:

  1. 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).
  2. 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 ).
  3. Introduce a mutex: add a state variable that locks the contract during code execution.

Analysis of the DAO exploit

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 is 0 will result in the number 255.
    • Adding 2^8=256 to a uint8 will leave the variable unchanged, as we have wrapped around the entire length of the uint.
  • overflow:
    • Adding numbers larger than the data type’s range is called an overflow.
    • Adding 257 to a uint8 that currently has a value of 0 will result in the number 1.

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).

PoWHC underflow

Batch Transfer Overflow (CVE-2018–10299)

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:

  1. 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)
  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

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, and msg.sender and msg.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 the fibonacciLibrary address (which will often be quite large, when interpreted as a uint). Thus it is likely that the withdraw function will revert, as it will not contain uint(fibonacciLibrary) amount of ether, which is what calculatedFibNumber will return.

    • fibonacciLibrary.delegatecall(fibSig, withdrawalCounter) modifies storage slot[1], which in current context is calculatedFibNumber (same as that in FibonacciLib). This is as expected.
  • The FibonacciBalance contract allows users to call all of the fibonacciLibrary functions via the fallback function. This includes the setStart function, which allows anyone to modify or set storage slot[0] in FibonacciBalance context.

    • An attacker could create a malicious contract, convert the address to a uint, and then call setStart(<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
        }
    }
    

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 the transfer function signature/selector, the next 32 bytes are the address, and the final 32 bytes represent the uint256 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 or send) fails; rather, the functions will simply return false.
  • 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 than send, as transfer will revert if the external transaction reverts. If send 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.

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 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.
  • 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:

  1. 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.
  2. Owner operations

    • If the privileged user loses their private keys or becomes inactive, the entire contract becomes inoperable.
  3. 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.

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 alias now 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 a 10 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 the 0x) for false or 0x000...1 (63 0s) for true.
  • 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 to unlocked.
    • newRecord.name = _name; and newRecord.mappedAddress = _mappedAddress; then set newRecord.name to _name and newRecord.mappedAddress to _mappedAddress.
      • This updates the storage locations of slot[0] and slot[1], which modifies both unlocked and the storage slot associated with registeredNameRecord.
    • This means that unlocked can be directly modified, simply by the bytes32 _name parameter of the register function.
      • If the last byte of _name is nonzero, it will modify the last byte of storage slot[0] and directly change unlocked to true. Such _name values will cause the require call require(unlocked); to succeed, as we have set unlocked to true.

Preventative techniques:

  • The Solidity compiler shows a warning for unintialized storage variables.
  • Explicitly use the memory or storage 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 uint256s 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).

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