X
Request an Audit
General information
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Blog
Cookie Consent

By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.

Cookie preferences
by
Gracious Igwe

Auditing A Solidity Contract: Episode 2 - Delegatecall

Smart contracts are self-executing codes that form the backbone of the Web3 ecosystem. Smart contracts serve as the foundational threads of the Web3 ecosystem, delicately balancing billions on an open network. Today, we'll discuss the delegatecall, one of the most common vulnerabilities that affect smart contracts. This is a great place to start if you want to learn about Solidity and how to audit smart contracts. This is one article in a series on auditing Solidity smart contracts. The series will cover vulnerabilities and resources that smart contract auditors use.

Delegatecall

In Solidity, there are two primary ways to interact with and send messages to contract functions. These methods are called Call and DelegateCall.

The Call function or opcode is used to start interactions between contracts by sending external messages. When you use the Call function, your code runs within the context of the external contract or function, either as the initiator or the recipient of the call. This function is handy for tasks like transferring gas or ether, as long as you provide the correct parameters.

DelegateCall works similarly to Call but with a crucial difference in execution: DelegateCall operates within the context of the caller rather than the recipient. Unlike Call, DelegateCall preserves the original values of msg.sender and msg.value. Essentially, DelegateCall maintains the context of the caller intact. It's important to ensure that the storage layout matches the caller and the receiver when using DelegateCall.

Despite the seemingly straightforward differences between Call and DelegateCall, the use of DelegateCall is quite tricky and can introduce unexpected outcomes in your code, leading to unwanted experiences.

When using DelegateCall, there are two essential considerations to bear in mind:

Victim Smart Contract

Let's look at how DelegateCall maintains context:

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

contract Lib{
  address public owner;
  
  function abc() public{
    owner = msg.sender;
  }
}

contract VictimContract{
  address public owner;
  Lib public lib;

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

  fallback() external payable{
    address(lib).delegatecall(msg.data);
  }
}

In the provided code snippet, the Victim contract employs DelegateCall to execute a call. At first glance, the code may not seem to permit changes to the Victim contract's owner. However, this initial impression can be misleading since a malicious actor could exploit vulnerabilities to gain control over the contract. Let's explore how this can happen:

The contract initializes the owner state variable within the constructor and includes a fallback function. Upon examining the code, it becomes evident that the fallback function uses DelegateCall. This function redirects the call to the lib state variable. Initially, this action might seem harmless, but its true consequences necessitate further investigation. 

So, how can we attack the Victim contract?

Attacker Smart Contract

To take control of the Victim contract or change who owns it, we need to change the owner address to the attacker's address. To do this, we have to figure out a way to communicate with the Victim contract no matter what. This can be done by using the fallback function.

Let us create a new contract and call it AttackerContract:

contract AttackerContract{
    address public victim;
    
    constructor(address _victim){ 
      victim = _victim;
    }
    
    function attack() public{
      victim.call(abi.encodeWithSignature("abc()"));
    }
}

Looking at the above code, the initial step involves creating a variable to hold the address of the Attacker contract. This variable's value will be set during contract deployment and fed into the constructor.

Within the contract, there exists a function named attack(). This function initiates a call to the Attacker contract. Upon closer examination, we notice that it attempts to invoke the abc() function in an entirely separate contract. Interestingly, the attack() function employs the function signature of abc() as its msg.data.

This maneuver executed by the attack() function triggers the fallback() function within the Attacker contract. A quick recall reveals that the fallback() function executes a DelegateCall to the Lib contract, forwarding the msg.data to it. 

So, how does this influence the Attacker contract?

Because the fallback function transfers the msg.data, which corresponds to the abc() function, to the Lib contract, the abc() function is activated. Consequently, the abc()  function updates the owner variable. As the delegatecall operates its code using the Victim contract's storage, the owner variable modified is the one belonging to the Victim contract. The abc() function alters the owner variable to match msg.sender. Since msg.sender refers to the initiator of the Victim contract, specifically AttackerContract, the new owner designation becomes AttackerContract.

Now, let's look at the arrangement of storage in Solidity:

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

contract Lib {
    uint public number;
    
    function action(uint _number) public {
      number = _number;
    }
}

contract Victim {
    address public lib;
    address public owner;
    uint public number;
    
    constructor(address _lib) {
      lib = _lib;
      owner = msg.sender;
    }

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

To understand this vulnerability, knowing how Solidity manages state variables is necessary.

We've established that when a DelegateCall is employed to modify storage in Solidity, the state variables must be declared in the same sequence. However, what if we overlook the correct order or specify the wrong type for these variables? The consequences can be undesirable.

The provided code has two contracts. The initial contract, Lib, introduces a state variable named number. Additionally, it features a function called action() that simply updates the number value.

Moving on to the second contract, Victim, three state variables are defined: lib, owner, and number. In the constructor, the contract assigns the lib value to the address of the Lib contract and designates the owner value as msg.sender.

The Victim contract also incorporates a function named action()" mirroring the behavior of Lib.action(). A DelegateCall is initiated using the Lib contract's address within this function. The DelegateCall request then targets the action() function within the Lib contract.

Examining this, we can observe that the Lib contract declares only one state variable, whereas the Victim contract declares three. This discrepancy is a vulnerability and a potential starting point for attackers aiming to exploit the Victim contract.

Now let's see how the Victim contract can be attacked because of this mistake:

contract AttackerContract {

      //By mirroring the storage layout of Vulnerable, the attacker 
      //gains the ability to accurately modify the state variables.
      
      address public lib;
      address public owner;
      uint public number;
      
      Victim public victim;
      
      constructor(Victim _victim) {
        victim = Victim(_victim);
      }
      
      function attack() public {
        //override address of lib
        victim.action(uint(uint160(address(this))));
        //call the function action() with any number as input. 
        victim.action(5);
      }
      
      // function signature must match Victim.action() 
        function action(uint _number) public {
          owner = msg.sender;
      }
}

In the given code snippet, the Attacker contract contains three state variables structured in the same arrangement as those within the Victim contract. Additionally, a state variable storing the address of the Victim contract is present, with its actual value assigned during the constructor execution.

The attacker defines an attack() function that makes two calls to the action() function inside the Victim contract.

In the first call, the attacker provides their address as an argument to Victim.action(). However, since Victim.action() expects a uint argument, the attacker shrewdly casts their address to uint. Upon execution of the first call, action() within the Victim contract is triggered. The number value becomes the attacker's address, converted to a uint. This function then initiates a DelegateCall to the Lib contract, which calls the action() function within it. This function updates the state variable within the Lib contract, setting it to the attacker's address.

Due to the specific storage layout, only the first variable is updated within the Victim contract. Since the initial variable in the Victim contract signifies the address of the Lib contract, it gets replaced with the Attacker contract's address.

With the completion of the first call's execution, attention shifts to the second call, victim.action(5). Here's what unfolds:

This call triggers the action() function within the Victim contract, as anticipated. However, it's important to note that victim.action() employs delegatecall using the value stored in the lib state variable. Considering that the lib variable was altered in the previous call, the function now performs a delegatecall to the Attacker contract.

Upon such a delegatecall, the Attacker.action() function is activated. In the corresponding code, the owner state variable is updated. However, the critical question arises: which owner state variable undergoes modification?

Because the entire operation unfolds within the context of the Victim contract, the owner state variable subject to modification belongs to the Victim contract. Furthermore, given that msg.sender denotes the attacker's address, the Victim contract's address transforms into the attacker's address, effectively establishing the attacker as the new owner of the Victim contract.

Once again, the contract is compromised due to the misuse of DelegateCall.

Recommendations For Deligatecall

To prevent this problem, you can use a special library in your code. This Library is called "stateless," which means it doesn't remember anything between different uses. It's similar to employing a tool that you reset to its original state after every use.

Using this stateless Library helps you ensure that the contracts you're working with don't accidentally remember the wrong things or mix up information when they talk to each other. This way, you create a safety net that stops the delegate call vulnerability from happening in your smart contract.

In Conclusion 

Delegatecall has the potential to result in substantial financial losses. Utilizing a stateless library offers a smarter and safer approach to constructing your contract while mitigating the risks associated with delegatecall vulnerabilities. It's akin to employing a specialized tool that guarantees each usage is isolated and doesn't interfere with prior information. Smart contract audits, bug bounties, and reviews are crucial in every stage of development. They increase the number of eyes scouting for vulnerabilities and decrease the chance of critical vulnerabilities slipping through. 

Stay safe. 

Related Articles:

Auditing A Solidity Contract: Episode 1 - Re-entrancy Attack

Auditing A Solidity Contract: Episode 3 - Security Analysis

Back to Blog
Latest
Latest