OneSwap Series 8 - The Evil Has a Name: Re-entrancy

in #oneswap4 years ago

In the development of smart contracts, an important issue that needs to be considered is the probability of re-entrancy attacks. One of the most typical cases is the attack suffered by the DAO project in 2016, which resulted in the theft of about 3.6 million ETH and, eventually, the hard fork of Ethereum from Ethereum Classic.

The principle of a re-entrancy attack is simple: smart contracts on Ethereum can call each other. Assume that Contract B, which is controlled by a hacker, is called during the execution of Contract A and the call of Contract A can be re-entered during the execution of Contract B. If Contract A does not update its internal state before calling the external contract, it may be misused by Contract B with assets being stolen.

Take the following Contract C as an example:

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

Contract C provides deposit and withdraw interfaces for ETH. Users can deposit a certain amount of ETH to the contract to use other functions provided by it, such as obtaining interest and voting. When they need to exit, they can call the withdraw interface to retrieve the deposited assets.

We can see that the withdraw method of Contract C first reads the total amount of funds deposited by the current trader, and then calls the call method to transfer ETH of this amount to the trader's account. But since msg.sender may be a contract account, the contract's fallback method is triggered on reception of ETH, and Contract C's withdraw method is called again in the fallback method. When re-entering the withdraw method, Contract C will once again read the total amount of funds deposited by the trader, which is exactly the same as last time. Therefore, the malicious contract receives the same amount of ETH again and re-enters the withdraw method of Contract C until its balance is exhausted.

Another concern is the cross-function re-entrancy attack. When some internal states are shared between the two methods of a contract X, another contract Y is called by one method, and then Y re-enters another method of X, causing similar consequences.

It's easy to prevent re-entrancy attacks: to apply the lock mechanism or to follow the Checks-effects-interactions mode when coding.

The lock mechanism adds a state variable inside the contract, which is used to identify whether the contract is re-entered. When a key method of the contract is called, it firstly verifies whether the contract is already in a locked state. If so, it reverts the transaction, If not, the lock will be locked, and will not be opened until the current method is done. Therefore, when the external contract re-enters the contract, the contract is already locked and thus immune to attacks.

Again, we take Contract C as an example. With the lock mechanism applied, the solution is as follows: Add the unlocked variable to the contract, which needs to be initialized to true when the contract is constructed. In addition, the modifier onlyUnlocked is added to the withdraw method, and assets can be withdrawn only when the contract is not in a re-entrant state.

contract C{
    bool unlocked;
    modifier onlyUnlocked{
            require(unlocked,"contract is already locked");
        unlocked = false;
        _;
        unlocked = true;
    }
    function deposit() external{
        ....
    }
  function withdraw() external onlyUnlocked{
      uint256 amount = balances[msg.sender];
      require(msg.sender.call.value(amount)());
      balances[msg.sender] = 0;  
  }
}


It is worth noting that in the above mechanism a lock variable unlocked is defined to identify whether the contract is unlocked, and a locked variable can also be defined to do the same job. The difference between the two is that the unlocked variable needs to be set to true during construction, while the locked variable skips this step.

The Checks-effects-interactions refers to the pattern that developers should first check the internal state when coding, then change it, and finally interact with external contracts. Taking Contract C as an example, the contract in this mode is written as follows:

contract C{
    function deposit() external{
        ....
    }
  function withdraw() external {
      uint256 amount = balances[msg.sender];
      require(amount != 0,"account balance is zero");
      balances[msg.sender] = 0;
      require(msg.sender.call.value(amount)());
  }
}

When a user withdraws assets, contracts C firstly verifies whether the account balance is 0. If yes, the transaction is reverted. If not, the account balance is set to 0, and then execute the actual transfer logic. At this point, when the fallback method of the malicious contract calls the withdraw method again, the attacker cannot withdraw more assets because the account balance has been cleared.

There is a case which requires no special consideration of re-entrancy attacks, that is, the contract does not save any internal state. In that case, even if the current contract is re-entered, no state will be changed as above mentioned, so severe consequences will never occur. This is the same case with the Router contract in the Oneswap project, which is only responsible for some calculations and the call forwarding to the Pair contract. Even if the contract is re-entered during the call, it just repeats calculations. What really matters is how the Pair contract prevents re-entrancy attacks.

Compared to EOA (externally owned account), transfers to contract accounts are full of risks, mainly because the called contract code is uncontrollable. You might suggest that we should distinguish between EOA and contract accounts through the extcodesize command and then check whether the contract bytecode of the recipient account is 0: if yes, it is an EOA; otherwise it is a contract account. But in fact, even if the return value of the extcodesize instruction is 0, we cannot say for sure that this is not a contract account. The extcodesize instruction obtains the contract code size of the corresponding account at the moment of the current call, and there is no available runtime code when the contract is initialized, so the return value of the extcodesize instruction is also 0 at this time. If the return value of the extcodesize command is not 0, then the account must be a contract account; otherwise it does not suggest that the account is an EOA.

Summary

This article introduces the principles and defensive measures of re-entrancy attacks that smart contracts may face, and illustrates situations in which re-entrancy attacks may not cause serious damage, with the Router contract of the Oneswap project as an example. Nevertheless, developers need to stay vigilant and try their best to block all possible entrances of re-entrancy attacks using the lock mechanism or Checks-effects-interactions mode when coding.

Coin Marketplace

STEEM 0.30
TRX 0.11
JST 0.034
BTC 66408.70
ETH 3225.39
USDT 1.00
SBD 4.17