Solidity Series Part 3: Call vs Delegatecall

Mantle Network
0xMantle
Published in
3 min readMay 17, 2023

--

Call and Delegatecall are both very commonly used in smart contract development, though they look similar and accept very similar parameters, they behave very differently. In this article we will explore the differences between the two.

Call

The call function is used to execute code from another contract, and can be used like so:

function myCall(address payable _to, bytes memory _data) public payable {
_to.call{value: msg.value}(_data);
}

When call is executed, the called contract runs in the context of itself. We can demonstrate this by calling address(this) on the called contract, which will return the called contract’s address.

This means when Contract A calls Contract B, it cannot access or modify the calling contract’s state variables as the execution context is in Contract B.

For example: we have two contracts: Contract A, and Contract B. Contract A has a function myCall that allows it to call any contract, while contract B has a function store that allows it to store arbitrary values.

contract ContractA {
function myCall(address payable _to, bytes memory _data) public payable {
_to.call{value: msg.value}(_data);
}
}

contract ContractB {
function store(uint256 _value, uint256 _slot) public {
assembly {
sstore(_slot, _value)
}
}
}

Using myCall from contract A to call contract B’s store will overwrite the storage slots in Contract B. For example, the following test function:

function testCall() public {
// Stores value 42 in slot 0
contractA.myCall(
payable(address(contractB)),
abi.encodeWithSignature("store(uint256,uint256)", 42, 0)
);
// Loads slot 0 in contract B
emit log_uint(uint256(vm.load(address(contractB), 0)));
}

Will yield us:

[PASS] testCall() (gas: 37890)
Logs:
42

Which shows us that the storage slot in contract B has been mutated.

Delegatecall

When delegatecall is used, the called contract’s function is executed in the context of the calling contract, calling address(this) should return the calling contract’s address.

This means that the called contract’s function can modify the state of the calling contract. This can be useful in situations where a contract needs to delegate some of its functionality to another contract while retaining control over its own state.

Let’s change Contract A’s call function to delegatecall and rename it to myDelegateCall:

contract ContractA {
function myDelegatecall(
address payable _to,
bytes memory _data
) public payable {
_to.delegatecall(_data);
}
}

Running the same testCall test function from above yields us:

[PASS] testCall() (gas: 37883)
Logs:
0

Why is this happening? This is because delegatecall will execute the called contract’s function in the context of the calling contract. Which means that the storage is written in contract A instead of contract B!

Once we change the address where the storage slot is loaded from contract B to contract A:

emit log_uint(uint256(vm.load(address(contractA), 0)));

We should get the following output:

[PASS] testCall() (gas: 37883)
Logs:
42

Conclusion

The differences between call and delegatecall are subtle, and it’s important to understand in order to use them effectively and securely. By understanding the execution context and the differences between call and delegatecall, you can write more effective, modular, and secure smart contracts in Solidity.

--

--

Mantle Network
0xMantle

Mantle | Mass adoption of decentralized & token-governed technologies. With Mantle Network, Mantle Treasury, and token holder-governed products initiatives.