Solidity: call() vs delegatecall()

Solidity: call() vs delegatecall()

tl;dr: delegatecall runs in the context of the caller contract.

The difference between call and delegatecall in Solidity relates to the execution context:

  • target.call(funcData):
    • the function reads/modifies target contract's storage
    • msg.sender is the caller contract
  • target.delegatecall(funcData)
    • the function reads/modifies caller contract's storage
    • msg.sender is the original sender == caller contract's msg.sender

![[Attachments/call.webp]]

![[Attachments/delegatecall.webp]]

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.24;

import "forge-std/Test.sol";

contract Target {
    address public owner;
    uint256 public value;

    function setOwnerAndValue(uint256 valueArg) public {
        owner = msg.sender;
        value = valueArg;
    }
}

contract Caller {
    address public owner;
    uint256 public value;

    function callSetOwnerAndValue(address target, uint256 valueArg) public {
        (bool success, ) = target.call(abi.encodeWithSignature("setOwnerAndValue(uint256)", valueArg));
        require(success, "call failed");
    }

    function delegatecallSetOwnerAndValue(address target, uint256 valueArg) public {
        (bool success, ) = target.delegatecall(abi.encodeWithSignature("setOwnerAndValue(uint256)", valueArg));
        require(success, "delegatecall failed");
    }
}

contract MyTest is Test {
    address sender = makeAddr("sender");
    Target target;
    Caller caller;

    function setUp() public {
        target = new Target();
        caller = new Caller();

        assertEq(target.owner(), address(0));
        assertEq(target.value(), 0);
        assertEq(caller.owner(), address(0));
        assertEq(caller.value(), 0);
    }

    function test_callSetOwnerAndValue() public {
        vm.prank(sender);
        caller.callSetOwnerAndValue(address(target), 100);

        // call modifies target contract's state, and target contract's msg.sender is caller contract
        assertEq(target.owner(), address(caller));
        assertEq(target.value(), 100);

        // caller contract's state didn't change
        assertEq(caller.owner(), address(0));
        assertEq(caller.value(), 0);
    }

    function test_delegatecallSetOwnerAndValue() public {
        vm.prank(sender);
        caller.delegatecallSetOwnerAndValue(address(target), 200);

        // target contract's state didn't change
        assertEq(target.owner(), address(0));
        assertEq(target.value(), 0);

        // delegatecall runs in the context of caller contract, so msg.sender is sender
        assertEq(caller.owner(), sender);
        assertEq(caller.value(), 200);
    }
}

ref:
https://medium.com/0xmantle/solidity-series-part-3-call-vs-delegatecall-8113b3c76855