DelegateCall()

Published by Mario Oettler on

The syntax of delegate call is similar to call. But there are a few differences in the behavior and the options.

The most important difference in the behavior to call is the context. With delegatecall, the target contract works in the same context as the calling contract. This means it can access variables of the calling contract and it sees the same global variables like msg.sender and msg.value.

With delegatecall, it is not possible to set a value and gas as options. The reason is fairly simple. Since the target contract operates in the same context as the calling contract, it can access the storage and balance of the calling contract.

Delegatecall allows us to code libraries.

Another point to remember is that if you use send, transfer, or call with a delegate call, you transfer the Ether from the calling contract (and not from the callee).

The following example shows the use of delegatecall.

pragma solidity 0.8.20;


contract callerContract{
    uint256 public value;
    address public sender;
    string  public name;
    bool    public callSuccess;
    
    constructor() payable{
  
    }
    
    function testDelegateCall(string memory _name, targetContractDelegate tc) public payable{
        value  = msg.value;
        sender = msg.sender;
        name   = _name;
        (bool success, bytes memory data) = payable(address(tc)).delegatecall(abi.encodeWithSignature("targetFunction(string)","Testname"));
        callSuccess = success;
    }
    
}

contract targetContractDelegate{
    uint256 public value;
    address public sender;
    string  public name;
      
    function targetFunction(string memory _nameTarget) public payable{
        value       = msg.value;
        sender      = msg.sender;
        name        = _nameTarget;
    }
}
    

Compile and deploy both contracts.

If you call the function testDelegateCall(), you have to pass a _name string. Let’s say, _name is “Bob”. The state variable name is then set to “Bob”. But in line 18, we “delegatecall” the target contract and pass “Testname”. In the target contract, “Testname” is stored in the same place like our variable name in the calling contract. Therefore, it overrides Bob.

If you read the variables from the targetContract, you will see that they are empty. This shows that a delegate call doesn’t change the target contract’s storage.

Now that we know that the delegate call writes to variables in the calling contract, we can check what msg.sender and msg.value we get. Both equal the values of the EOA that initiated the transaction.

Now, we want to learn a bit more about this overriding behavior. The EVM assigns variables to slots. The order of the slots is given by the order of the variables defined in the contract. If a variable value is changed, the EVM looks into the slot and reads/writes the data in it. In our calling contract, this looks like that:

In the case of delegatecall, the target contract accesses the same storage slots as the calling contract. And again, the order of the slots is defined by the order of the variables. But this time, the order of the variables in the target contract defines the order in which the variables are stored.

In our example above, this looked like that:

But what if we change the order so that it doesn’t align with the calling contract anymore? Let’s try this by swapping the position of the variable sender and value.

pragma solidity 0.8.20;


contract callerContract{
    uint256 public value;
    address public sender;
    string  public name;
    bool    public callSuccess;
    
    constructor() payable{
  
    }
    
    function testDelegateCall(string memory _name, targetContractDelegate tc) public payable{
        value  = msg.value;
        sender = msg.sender;
        name   = _name;
        (bool success, bytes memory data) = payable(address(tc)).delegatecall(abi.encodeWithSignature("targetFunction(string)","Testname"));
        callSuccess = success;
    }
    
}

contract targetContractDelegate{
    
    address public sender;
    uint256 public value;
    string  public name;
    address public senderLocal;
    
    function targetFunction(string memory _nameTarget) public payable{
        value       = msg.value;
        sender      = msg.sender;
        senderLocal = msg.sender;
        name        = _nameTarget;
    }
}
    

If you now invoke the function testDelegateCall() in the caller contract and check the values of the variables sender and value, you will see that sender is 0x0000… and value is a pretty big number. Instead of storing the msg.sender address to sender it has been stored to value. And the value has been stored to sender.

Do not use delegatecall to contracts you do not know. Using delegatecall is a security risk in this case because it allows the target contract to manipulate your contract’s data.

Categories:

if()