Smart Contract Attack Vectors

As of April 26th 2022

Attacks

Front-Running / Transaction-Ordering Dependence

FrontRunning is defined as follows, “A course of action where an entity benefits from prior access to privileged market information about upcoming transactions and trades”. Such knowledge can lead to exploitation.

For example, knowing that a large purchase of a token is going to occur – an attacker can purchase that coin in advance and sell for profit when the large buy-order increases the price.

Since the solution to this problem varies on per contract basis, it can be hard to protect against. Some possible solutions include batching transactions and using a pre-commit scheme.

DoS with Block Gas Limit

On the blockchain, all the blocks have a gas limit. One of the benefits of gas, is that it prevents attackers from creating an infinite transaction loop, but if the gas usage of a transaction exceeds this limit – the transaction fails. This could lead to a DoS attack in a couple of different ways.

  1. Unbounded Operations

A situation in which the block gas limit can be an issue is in sending funds to an array of addresses. Even without any malicious intent, this could go easily wrong. Just by having too large an array of users to pay – can max out the gas limit and prevent the transaction from ever succeeding.

This could lead to an attack, for example, a hacker decides to create a significant amount of addresses with each address being paid a small amount of funds from the smart contract. If done effectively, the transaction can be blocked indefinitely, possible even preventing further transactions from going through.

An effective solution to this problem, would be to use a pull payment system over the current push payment system. To do this, separate each payment into its own transaction, and have the recipient call the function.

If for some reason, you really need to loop through an array of unspecified length, at least expect it to potentially take multiple blocks, and allow it to be performed in multiple transactions.

  1. Block Stuffing

In some situations, your contract can be attacked with a block gas limit

Even if you dont loop through an array of unspecified length. An attacker can fill several blocks before a transaction can be processed by using a sufficiently high gas price.

This attack is done by issuing several transactions at a very high gas price. If the gas price is high enough, and the transactions consume enough gas, they can fill entire blocks and prevent other transactions from being processed.

Ethereum transactions require the sender to pay gas to disincentivize spam attacks, but in some situations there can be enough incentive to go through with such an attack. For example, a block stuffing attack was used on a gambling Dapp. The app had a countdown timer, users could win a jackpot by being the last to purchase a key, except every time a user bought a key, the timer would be extended – An attacker bought a key then stuffed the next 13 blocks in a row so they could win the jackpot.

To prevent such attacks from occurring, its important to carefully consider whether its safe to incorporate time-based actions in your application.

    DoS with Revert

    DoS can occur in functions when you try to send funds to a user and the functionality relies on that fund transfer being successful.

    This could be problematic in the case that the funds are sent to a smart contract created by a bad actor, since they can simply create a fallback function that reverts all payments.

    Insecure Implementation Example ..

    contract Auction {
        address currentLeader;
        uint highestBid;
    
        function bid() payable {
            require(msg.value > highestBid);
    
            require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert
    
            currentLeader = msg.sender;
            highestBid = msg.value;
        }
    }
    

    From this example, if an attacker bids from a smart contract with a fallback function reverting all payments, they can never be refunded, and thus no one can ever make a higher bid.

    This could be problematic without an attacker, if for example, you want to pay an array of users by iterating through the array, and of course you want to make sure each user is properly paid. The problem here is that if one payment fails, the function is reverted and no one is paid.

    address[] private refundAddresses;
    mapping (address => uint) public refunds;
    
    // bad
    function refundAll() public {
        for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
            require(refundAddresses[x].send(refunds[refundAddresses[x]])) // doubly bad, now a single failure on send will hold up all funds
        }
    }
    

    An effective solution to this problem would be to use a pull payment system over the current push payment system. To do this, separate each payment into its own transaction, and have the recipient call the function.

    Solution Example ..

    contract auction {
        address highestBidder;
        uint highestBid;
        mapping(address => uint) refunds;
    
        function bid() payable external {
            require(msg.value >= highestBid);
    
            if (highestBidder != address(0)) {
                refunds[highestBidder] += highestBid; // record the refund that this user can claim
            }
    
            highestBidder = msg.sender;
            highestBid = msg.value;
        }
    
        function withdrawRefund() external {
            uint refund = refunds[msg.sender];
            refunds[msg.sender] = 0;
            (bool success, ) = msg.sender.call.value(refund)("");
            require(success);
        }
    }
    

    Forcibly Sending Ether to a Contract

    Occasionally, it is unwanted for users to be able to send Ether to a smart contract. Unfortunately for these circumstances, its possible to bypass a contract fallback function and forcibly send Ether.

    contract Vulnerable {
        function () payable {
            revert();
        }
    
        function somethingBad() {
            require(this.balance > 0);
            // Do something bad
        }
    }
    

    Though it seems like any transaction to the Vulnerable contract should be reverted, there are actually a couple of ways to forcibly send Ether.

    The first method is to call the selfdestruct method on a contract with the Vulnerable contract address set as a the beneficiary. This works because selfdestruct will not trigger the fallback function.

    Another method is to precompute a contracts address and send Ether to the address before the contract is even deployed.

    Insufficient Gas Griefing

    Griefing is a type of attack often performed in video games, where a malicious user plays a game in an unintended way to bother other players, also known as trolling. This type of attack is also used to prevent transactions from being performed as intended.

    This attack can be done on contracts which accept data and use it in a sub-call on another contract. This method is often used in multisignature wallets as well as transaction relayers. If the sub-call fails, either the whole transaction is reverted or execution is continued.

    Lets consider a simple relayer contract as an example, as shown below, the relayer contract allows someone to make and sign a transaction, without having to execute the transaction. Often this is used when a user cant pay for the gas associated with the transaction.

    contract Relayer {
        mapping (bytes => bool) executed;
    
        function relay(bytes _data) public {
            // replay protection; do not call the same transaction twice
            require(executed[_data] == 0, "Duplicate call");
            executed[_data] = true;
            innerContract.call(bytes4(keccak256("execute(bytes)")), _data);
        }
    }
    

    The user who executes the transaction, the ‘forwarder’, can effectively censor transactions by using just enough gas so that the transaction executes, but not enough gas for the sub-call to succeed.

    There are two ways this could be prevented. The first solution would be to only allow trusted users to relay transactions. The other solution is to require that the forwarder provides enough gas, as seen below..

    // contract called by Relayer
    contract Executor {
        function execute(bytes _data, uint _gasLimit) {
            require(gasleft() >= _gasLimit);
            ...
        }
    }
    

    Re-entrancy

    Reentrancy is an attack that can occur when a bug in a contract function can allow function interaction to proceed multiple times when it should otherwise be prohibited. This can be used to drain funds from a smart contract is used maliciously.

    In fact, reentrancy, was the attack vector used in the DAO hack.

    Let us review two forms of the functions …

    Single Function Re-entrancy

      A single function reentrancy attack occurs when a vulnerable function is the same function that an attacker is trying to recursively call..

      function withdraw() external {
          uint256 amount = balances[msg.sender];
          require(msg.sender.call.value(amount)());
          balances[msg.sender] = 0;
      }
      

      Here we can see that the balance is only modified after the funds have been transferred. This can allow a hacker to call the function many times before the balance is set to 0, effectively draining the smart contract.

      Cross Function Re-entrancy


        A cross function reentrancy attack is a more complex version of the same process. Cross function re-entrancy occurs when a vulnerable function shares state with a function that an attacker can exploit.

        function transfer(address to, uint amount) external {
          if (balances[msg.sender] >= amount) {
            balances[to] += amount;
            balances[msg.sender] -= amount;
          }
        }
        
        function withdraw() external {
          uint256 amount = balances[msg.sender];
          require(msg.sender.call.value(amount)());
          balances[msg.sender] = 0;
        }
        

        In this example, a hacker can exploit this contract by having a fallback function call transfer() to transfer spent funds before the balance is set to 0 in the withdraw() function.

        Re-Entrancy Prevention

        When transferring funds in a smart contract, use send or transfer  instead of call. The problem using call is that unlike the other functions, it doesn’t have a gas limit of 2300. This means that call can be used in external function calls which can be used to perform re-entrancy attacks.
        Another solid prevention method is to mark untrusted functions

        function untrustedWithdraw() public {
          uint256 amount = balances[msg.sender];
          require(msg.sender.call.value(amount)());
          balances[msg.sender] = 0;
        }

        In addition, for optimum security use the checks-effects-interactions pattern. This simple rule of thumb for ordering smart contract functions.

        The function should begin with checks like require and assert statements.

        Next, the effects of the contract should be performed, like state modifications.

        Finally, we can perform interactions with other smart contracts, like external function calls.

        This structure is effective against re-entrancy because the modified state of the contract will prevent bad actors from performing malicious interactions.

        function withdraw() external {
          uint256 amount = balances[msg.sender];
          balances[msg.sender] = 0;
          require(msg.sender.call.value(amount)());
        }

        Since the balance is set to 0 before any interactions are performed, if the contract is called recursively, there is nothing to send after the first transaction.

        HoneyPot

        A honeypot is a type of smart contract attack that appears to be a vulnerable contract, but is actually just a trap. Honeypots work by luring attackers with a balance stored in the smart contract, and what appears to be a vulnerability in the code.

        Typically ,to access the funds, the attacker would have to send their own funds but unbeknownst to them, there is some kind of recovery mechanism, allowing the smart contract owner to recover their own funds along with the funds of the attacker.

        pragma solidity ^0.4.18;
        
        contract MultiplicatorX3
        {
            address public Owner = msg.sender;
           
            function() public payable{}
           
            function withdraw()
            payable
            public
            {
                require(msg.sender == Owner);
                Owner.transfer(this.balance);
            }
            
            function Command(address adr,bytes data)
            payable
            public
            {
                require(msg.sender == Owner);
                adr.call.value(msg.value)(data);
            }
            
            function multiplicate(address adr)
            public
            payable
            {
                if(msg.value>=this.balance)
                {        
                    adr.transfer(this.balance+msg.value);
                }
            }
        }

        In this contract, it seems that by sending more than the contract balance to multiplicate() you can set your address as the contract owner, then proceed to drain the contract of funds. 

        However, although it seems that this.balance is updated after the function is executed, it is actually updated before the function is called, meaning that multiplicate() is never executed, yet the attackers funds are locked in the contract.

        Lets review another one …

        pragma solidity ^0.4.19;
        
        contract Gift_1_ETH
        {
            bool passHasBeenSet = false;
            
            function()payable{}
            
            function GetHash(bytes pass) constant returns (bytes32) {return sha3(pass);}
            
            bytes32 public hashPass;
            
            function SetPass(bytes32 hash)
            public
            payable
            {
                if(!passHasBeenSet&&(msg.value >= 1 ether))
                {
                    hashPass = hash;
                }
            }
            
            function GetGift(bytes pass)
            external
            payable
            {
                if(hashPass == sha3(pass))
                {
                    msg.sender.transfer(this.balance);
                }
            }
            
            function PassHasBeenSet(bytes32 hash)
            public
            {
                if(hash==hashPass)
                {
                   passHasBeenSet=true;
                }
            }
        }

        This contract is especially sneaky. So long as passHasBeenSet , anyone could GetHash(), SetPass()  and GetGift(). This sneaky part of this contract, is that the last sentence is entirely true, even though its not in the etherscan transaction log.

        You see, when smart contracts make transactions to weather other they dont appear in the transaction log, this is because they perform whats known as a message call and not a transaction. So what happened here, must have been some external contract setting the pass before anyone else could.
        A safer method the attacker should have used would have been to check the contract storage with security analysis tool, such as Mythril.

        Scroll to Top
        Skip to content