Smart Contract Security Audit

A smart contract is an automated transaction protocol that executes the terms of a contract. They are one of the most exciting areas of blockchain technology implementation. 

The audit of a Smart Contract is technically the same as auditing a regular code. It entails meticulously investigating code to find security flaws and vulnerabilities before publicly deploying the code. In addition, it involves developers scrutinizing the code that is used to underwrite the terms of the smart contract. 

An effective Smart Contract audit should focus on the following areas;

  • Common errors such as stack problems, compilation, and reentrance mistakes.
  • Any known errors or security flaws of the Smart contract’s host platform.
  • Simulating attacks on the Smart Contract. 

Bad randomness

Pseudorandom number generation on the blockchain is generally unsafe. There are a number of reasons for this, including:

  • The blockchain does not provide any cryptographically secure source of randomness. Block hashes in isolation are cryptographically random, however, a malicious miner can modify block headers, introduce additional transactions, and choose not to publish blocks in order to influence the resulting hashes. Therefore, miner-influenced values like block hashes and timestamps should never be used as a source of randomness.

Attack Scenarios

  • A lottery where people bet on whether the hash of the current block is even or odd. A miner that bets on even can throw out blocks whose hash are even.
  • A commit-reveal scheme where users don’t necessarily have to reveal their secret (to prevent DoS). A user has money riding on the outcome of the PRG and submits a large number of commits, allowing them to choose the one they want to reveal at the end.

Mitigations

There are currently not any recommended mitigations for this issue. Do not build applications that require on-chain randomness. In the future, however, these approaches show promise

  • Verifiable delay functions: functions which produce a pseudorandom number and take a fixed amount of sequential time to evaluate
  • Randao: A commit reveal scheme where users must stake wei to participate

Vulnerable Code

The random function in theRun was vulnerable to this attack. It used the blockhash, timestamp and block number to generate numbers in a range to determine winners of the lottery. To exploit this, an attacker could set up a smart contract that generates numbers in the same way and submits entries when it would win. As well, the miner of the block has some control over the blockhash and timestamp and would also be able to influence the lottery in their favor.

  function random(uint Max) constant private returns (uint256 result){        //get the best seed for randomness        uint256 x = salt * 100 / Max;        uint256 y = salt * block.number / (salt % 5) ;        uint256 seed = block.number/3 + (salt % 300) + Last_Payout +y;         uint256 h = uint256(block.blockhash(seed));             return uint256((h / x)) % Max + 1; //random number between 1 and Max    }

Denial of Service

A malicious contract can permanently stall another contract by failing in a strategic way. In particular, contracts that bulk perform transactions or updates using a for loop can be DoS’d if a call to another contract or transfer fails during the loop.

Attack Scenarios

  • Auction contract where frontrunner must be reimbursed when they are outbid. If the call refunding the frontrunner continuously fails, the auction is stalled and they become the de facto winner.
  • Contract iterates through an array to pay back its users. If one transfer fails in the middle of a for loop all reimbursements fail.
  • Attacker spams contract, causing some array to become large. Then for loops iterating through the array might run out of gas and revert.

Mitigations

  • Favor pull over push for external calls
  • If iterating over a dynamically sized data structure, be able to handle the case where the function takes multiple blocks to execute. One strategy for this is storing iterator in a private variable and using while loop that exists when gas drops below certain threshold.

Vulnerable Code

Bulk refund functionality that is suceptible to DoS

address[] private refundAddresses;  mapping(address => uint) public refundAmount;
  function refundDos() public {    for(uint i; i < refundAddresses.length; i++) {      require(refundAddresses[i].transfer(refundAmount[refundAddresses[i]]));    }  }}
contract CrowdFundPull {  address[] private refundAddresses;  mapping(address => uint) public refundAmount;
  function withdraw() external {    uint refund = refundAmount[msg.sender];    refundAmount[msg.sender] = 0;    msg.sender.transfer(refund);  }}

Contracts can be forced to receive ether

In certain circunstances, contracts can be forced to receive ether without triggering any code. This should be considered by the contract developers in order to avoid breaking important invariants in their code.

Attack Scenarios

  • An attacker can use a specially crafted contract to forceful send ether using suicide / selfdestruct:
contract Sender {  function receive_and_suicide(address target) payable {    suicide(target);  }}

Mitigations

  • There is no way to block the reception of ether. The only mitigation is to avoid assuming how the balance of the contract increases and implement checks to handle this type of edge cases.

Vulnerable Code

The MyAdvancedToken contract is vulnerable to this attack. It will stop the owner to perform the migration of the contract.

mapping (address => bool) public frozenAccount;
    /* This generates a public event on the blockchain that will notify clients */    event FrozenFunds(address target, bool frozen);
    /* Initializes contract with initial supply tokens to the creator of the contract */    function MyAdvancedToken(        string tokenName,        string tokenSymbol    ) TokenERC20(tokenName, tokenSymbol) public {}
    /* Internal transfer, only can be called by this contract */    function _transfer(address _from, address _to, uint _value) internal {        require (_to != 0x0);                               // Prevent transfer to 0x0 address.        require (balanceOf[_from] >= _value);               // Check if the sender has enough        require (balanceOf[_to] + _value >= balanceOf[_to]); // Check for overflows        require(!frozenAccount[_from]);                     // Check if sender is frozen        require(!frozenAccount[_to]);                       // Check if recipient is frozen        balanceOf[_from] -= _value;                         // Subtract from the sender        balanceOf[_to] += _value;                           // Add the same to the recipient        emit Transfer(_from, _to, _value);    }
    /// @notice Buy tokens from contract by sending ether    function buy() payable public {        uint amount = msg.value;                          // calculates the amount balanceOf[msg.sender] += amount;                  // updates the balance        totalSupply += amount;                            // updates the total supply        _transfer(address(0x0), msg.sender, amount);      // makes the transfer    }
    /* Migration function */    function migrate_and_destroy() onlyOwner { assert(this.balance == totalSupply);                 // consistency check                                     suicide(owner);                                      // transfer the ether to the owner and kill the contract    }

Incorrect interface

A contract interface defines functions with a different type signature than the implementation, causing two different method id’s to be created. As a result, when the interface is called, the fallback method will be executed.

Attack Scenarios

  • The interface is incorrectly defined. Alice.set(uint) takes an uint in Bob.sol but Alice.set(int) a int in Alice.sol. The two interfaces will produce two differents method IDs. As a result, Bob will call the fallback function of Alice rather than of set.

Mitigations

  • Verify that type signatures are identical between interfaces and implementations.

Vulnerable Code

// Wait to mine the block containing the transaction
var alice = contractAlice.at(contractPartialInstanceAlice.address);
var contractBob = eth.contract(abiBob); var txDeployBob = {from:eth.coinbase, data: bytecodeBob, gas: 1000000}; var contractPartialInstanceBob = contractBob.new(txDeployBob); 
// Wait to mine the block containing the transaction
var bob = contractBob.at(contractPartialInstanceBob.address);
// From now, wait for each transaction to be mined before calling// the others transactions
// print the default value of val: 0alice.val() 
// call bob.set, as the interface is wrong, it will call// the fallback function of alicebob.set(alice.address, {from: eth.accounts[0]} )// print val: 1alice.val()
// call the fixed version of the interfacebob.set_fixed(alice.address, {from: eth.accounts[0]} )// print val: 42alice.val()

Integer Overflow

It is possible to cause add and sub to overflow (or underflow) on any type of integer in Solidity.

Attack Scenarios

  • Attacker has 5 of some ERC20 token. They spend 6, but because the token doesn’t check for underflows, they wind up with 2^256 tokens.
  • A contract contains a dynamic array and an unsafe pop method. An attacker can underflow the length of the array and alter other variables in the contract.

Mitigations

  • Use openZeppelin’s safeMath library
  • Validate all arithmetic

Vulnerable Code

// SPDX-License-Identifier: GPL-3.0pragma solidity 0.8.0;
contract RolloverExample2 {    uint8 public myUint8;
    function decrement() public {        myUint8–;    }
    function increment() public {        myUint8++;    }}

Race Condition

There is a gap between the creation of a transaction and the moment it is accepted in the blockchain. Therefore, an attacker can take advantage of this gap to put a contract in a state that advantages them.

Attack Scenarios

  • Bob creates RaceCondition(100, token). Alice trusts RaceCondition with all its tokens. Alice calls buy(150) Bob sees the transaction, and calls changePrice(300). The transaction of Bob is mined before the one of Alice and as a result, Bob received 300 tokens.
  • The ERC20 standard’s approve and transferFrom functions are vulnerable to a race condition. Suppose Alice has approved Bob to spend 100 tokens on her behalf. She then decides to only approve him for 50 tokens and sends a second approve transaction. However, Bob sees that he’s about to be downgraded and quickly submits a transferFrom for the original 100 tokens he was approved for. If this transaction gets mined before Alice’s second approve, Bob will be able to spend 150 of Alice’s tokens.

Mitigations

  • For the ERC20 bug, insist that Alice only be able to approve Bob when he is approved for 0 tokens.
  • Keep in mind that all transactions may be front-run

Vulnerable Code

// If the owner sees someone calls buy    // he can call changePrice to set a new price    // If his transaction is mined first, he can    // receive more tokens than excepted by the new buyer    function buy(uint new_price) payable        public    {        require(msg.value >= price);
        // we assume that the RaceCondition contract        // has enough allowance        token.transferFrom(msg.sender, owner, price);
        price = new_price;        owner = msg.sender;    }
    function changePrice(uint new_price){        require(msg.sender == owner);        price = new_price;     }

Re-entrancy

A state variable is changed after a contract uses call.value. The attacker uses a fallback function—which is automatically executed after Ether is transferred from the targeted contract—to execute the vulnerable function again, before the state variable is changed.

Attack Scenarios

  • A contract that holds a map of account balances allows users to call a withdraw function. However, withdraw calls send which transfers control to the calling contract, but doesn’t decrease their balance until after send has finished executing. The attacker can then repeatedly withdraw money that they do not have.

Mitigations

  • Avoid use of call.value
  • Update all bookkeeping state variables before transferring execution to an external contract.

Vulnerable Code

function withdrawRewardFor(address _account) noEther internal returns (bool _success) {  if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])    throw;
  uint reward =    (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply – paidOut[_account];
  reward = rewardAccount.balance < reward ? rewardAccount.balance : reward;
  paidOut[_account] += reward; // XXXXX  if (!rewardAccount.payOut(_account, reward)) // XXXXX    throw;
  return true;}

Unchecked External Call

Certain Solidity operations known as “external calls”, require the developer to manually ensure that the operation succeeded. This is in contrast to operations which throw an exception on failure. If an external call fails, but is not checked, the contract will continue execution as if the call succeeded. This will likely result in buggy and potentially exploitable behavior from the contract.

Attack Scenarios

  • A contract uses an unchecked address.send() external call to transfer Ether.
  • If it transfers Ether to an attacker contract, the attacker contract can reliably cause the external call to fail, for example, with a fallback function which intentionally runs out of gas.
  • The consequences of this external call failing will be contract specific.
    • In the case of the King of the Ether contract, this resulted in accidental loss of Ether for some contract users, due to refunds not being sent.

Mitigations

  • Manually perform validation when making external calls
  • Use address.transfer()

Vulnerable Code

      // If they paid too little, reject claim and refund their money.        if (valuePaid < currentClaimPrice) {            msg.sender.send(valuePaid);            return;        }
        // If they paid too much, continue with claim but refund the excess.        if (valuePaid > currentClaimPrice) {            uint excessPaid = valuePaid – currentClaimPrice;            msg.sender.send(excessPaid);            valuePaid = valuePaid – excessPaid;        }

Unprotected function

Missing (or incorrectly used) modifier on a function allows an attacker to use sensitive functionality in the contract.

Attack Scenarios

  • A contract with a changeOwner function does not label it as private and therefore allows anyone to become the contract owner.

Mitigations

  • Always specify a modifier for functions.

Vulnerable Code

An onlyOwner modifier is defined but not used, allowing anyone to become the owner

      pragma solidity ^0.4.15;
contract Unprotected{    address private owner;
    modifier onlyowner {        require(msg.sender==owner);        _;    }
    function Unprotected()        public     {        owner = msg.sender;    }
    // This function should be protected    function changeOwner(address _newOwner)         public    {       owner = _newOwner;    }
    function changeOwner_fixed(address _newOwner)         public         onlyowner    {       owner = _newOwner;    }}

Variable Shadowing

Variable shadowing occurs when a variable declared within a certain scope (decision block, method, or inner class) has the same name as a variable declared in an outer scope.

Attack Scenarios

  • This depends a lot on the code of the contract itself.

Mitigations

  • The solidity compiler has some checks to emit warnings when it detects this kind of issue.

Vulnerable Code

variable shadowing prevents the owner of contract C from performing self destruct

contract Suicidal {  address owner;  function suicide() public returns (address) {    require(owner == msg.sender);    selfdestruct(owner);  }}contract C is Suicidal {  address owner;  function C() {    owner = msg.sender;  }}
    function changeOwner_fixed(address _newOwner)         public         onlyowner    {       owner = _newOwner;    }}

Wrong Constructor Name

A function intended to be a constructor is named incorrectly, which causes it to end up in the runtime bytecode instead of being a constructor.

Attack Scenarios

  • Anyone can call the function that was supposed to be the constructor. As a result anyone can change the state variables initialized in this function.

Mitigations

  • Use constructor instead of a named constructor

Vulnerable Code

pragma solidity ^0.4.15;
contract Missing{    address private owner;
    modifier onlyowner {        require(msg.sender==owner);        _;    }
    // The name of the constructor should be Missing    // Anyone can call the IamMissing once the contract is deployed    function IamMissing()        public     {        owner = msg.sender;    }
    function withdraw()         public         onlyowner    {       owner.transfer(this.balance);    }}

Top 8 Smart Contract Security Tools

Mythril

Security analysis tool for EVM bytecode. Supports smart contracts built for Ethereum, Hedera, Quorum, Vechain, Roostock, Tron and other EVM-compatible blockchains.

For example:

myth a killbilly.sol -t 3

sabre

Security analyzer for Solidity smart contracts. Uses the MythX smart contract security service.

For example:

sabre check <solidity-file> [contract-name]

smartbugs

SmartBugs is an execution framework aiming at simplifying the execution of analysis tools on datasets of smart contracts.

For example:

python3 smartBugs.py –tool all –file dataset/reentrancy/simple_dao.sol

truffle

Truffle is a development environment, testing framework and asset pipeline for Ethereum, aiming to make life as an Ethereum developer easier. With Truffle, you get:

  • Built-in smart contract compilation, linking, deployment and binary management.
  • Automated contract testing with Mocha and Chai.
  • Configurable build pipeline with support for custom build processes.
  • Scriptable deployment & migrations framework.
  • Network management for deploying to many public & private networks.
  • Interactive console for direct contract communication.
  • Instant rebuilding of assets during development.
  • External script runner that executes scripts within a Truffle environment.

For example:

truffle test ./test/TestMetaCoin.sol

solhint

Solhint is an open source project created by https://protofire.io. Its goal is to provide a linting utility for Solidity code.

For example:

solhint contracts/MyToken.sol

Ethlint

Ethlint (Formerly Solium) analyzes your Solidity code for style & security issues and fixes them.

For example:

solium -f foobar.sol

echidna

Ethereum smart contract fuzzer

For example:

echidna-test myContract.sol

securify

Security Scanner for Ethereum Smart Contracts

For example:

java -jar build/libs/securify.jar -fs src/test/resources/solidity/transaction-reordering.sol

Top Resource for Learning

Fundamentals of Smart Contract Security

  • Explore smart contract fundamentals, including the Ethereum protocol, Solidity programming language, and the Ethereum Virtual Machine
  • Dive into smart contract development using Solidity and gain experience with Truffle framework tools for deploying and testing your contracts
  • Use Web3 to connect your smart contracts to an applicationso users can easily interact with the blockchain
  • Examine smart contract security along with free online resources for smart contract security auditing

Truffle Quick Start Guide

  • Build your first Ethereum Dapp with Truffle: the most popular Ethereum development framework
  • Build, compile, and deploy smart contracts in your development environment
  • Embrace Blockchains and utilize it to create new generation of secured and scalable apps

Security Checklist

Security Checklist

Resources

  • https://blog.csdn.net/ityouknow/article/details/121896509
  • https://blog.csdn.net/smellycat000/article/details/120213401
  • https://blog.csdn.net/smellycat000/article/details/119814296
  • https://github.com/nomi-sec/PoC-in-GitHub

Related Content

CONNECT WITH US

FEATURED ARTICLES

Subscribe

Sign up now to receive the latest notifications and updates from Hadess.

Sign up for News & Communications

Do you want quick & free cyber-security analysis of your application?

Secure your entire workforce, including remote employees.

TRY IT FREE

FOR 15 DAYS