Solidity: Delegatecall vs Call vs Library

  • October 30, 2020
  • Michał Mirończuk

Introduction 💬

One of the most important aspects in solidity programming are low level delegatecall, call functions as well as use of libraries. In this series of posts I gonna try to explain you how they work, in what context they work in as well as show you potential vulnerabilities ☠️ and unsafe ⚡ use.

First of all I want to emphasize that in this post I have used solidity version 0.7.0. I think that is a thing worth mentioning because of the dynamic development of the solidity language 😉.


Demonstration Code ✏️

By these 30 lines of code below we are able to fish out and test necessary behaviours of the functions as well as the context which they are located. 😎😎😎

Take a look...

pragma solidity ^0.7.0;

contract ImplementationContract {
    event testEvent(address txOrigin, address msgSenderAddress, address _from, uint msgValue);
    function doSomething() public payable {
        emit testEvent(tx.origin, msg.sender, address(this), msg.value);
    }
}

library ImplementationLib {
    event testEvent(address txOrigin, address msgSenderAddress, address _from, uint msgValue);
    function doSomething() public {
        emit testEvent(tx.origin, msg.sender, address(this), msg.value);
    }
}
contract CallingContract {
   address implementationContractAddress = address(new ImplementationContract());
   
   function callImplementationContract() payable public {
       implementationContractAddress.call{value: 0.5 ether}(abi.encodeWithSignature("doSomething()"));
   }
   
   function delegateCallToImplementationContract() payable public {
       implementationContractAddress.delegatecall(abi.encodeWithSignature("doSomething()"));
   }
    
    function callImplementationLib() payable public {
       ImplementationLib.doSomething();
   }
}

In the code snippet we can distinguish exactly 3 entities. Going from the top there are ImplementationContract contract and ImplementationLib library where logic is implemented. Below them CallingContract as a third entity which we will use as a proxy to invoke logic using ours today's heroic functions: delegatecall, call and library call. (ImplementationLib) 🦸🦸‍♂️🦸🏻‍♀️


Graphical presentation:

CallingContract
Callin...
callImplementationContract()
callIm...
delegateCallToImplementationContract()
delega...
callImplementationLib()
callIm...
ImplementationContract
Implem...
doSomething()
doSomething()
ImplementationLib
Implem...
doSomething()
doSomething()
call
call
delegatecall
delegatecall
call
call
Viewer does not support full SVG 1.1

Transaction properties (tx.origin, msg.sender, msg.value) ✏️

Notice that ImplementationContract as well as ImplementationLib emit testEvents embodied as:


    event testEvent(address txOrigin, address msgSenderAddress, address _from, uint msgValue);
    

They are emitted in doSomething() methods. Thanks to them we can easly elicit calling context with transaction properties depending on the function (delegatecall or call) used.

Let's run the functions and see the events' outputs for each call! 🔥

Working in Remix IDE, after contract deployment we have got following addresses:

  • User wallet address: 0x5B3...dC4
  • CallingContract address: 0xD7A...71B
  • ImplementationContract address: 0xd84...397
  • ImplementationLib of course has no address

Transactions have been invoked one by one with 1 ether in value.

---------tx.originmsg.senderfrommsg.value
delegatecall0x5B3...dC4 (User)0x5B3...dC4 (User)0xD7A...71B (CallingContract)1
call0x5B3...dC4 (User)0xD7A...71B (CallingContract)0xd84...397 (ImplementationContract)0
library call 0x5B3...dC4 (User)0x5B3...dC4 (User)0xD7A...71B (CallingContract)1

Conclusion ✏️

  • Call

Now we are able to say that using call function to the ImplementationContract context changes, it calls an instance (instance here is an important word) of the ImplementationContract. Transaction properties are different in comparison to delegated call and library call.

  • Delegatecall & library call

Delegatecall as well as library call do not change a context. It means that caller imports, pulls code into itself and uses calling function in it's own context.

Good to know 💡💡💡
Futhermore, the example shows us tx.origin property behaviour. It gives the origin of the transaction. A tx.origin property address will be always a user wallet address.
Stackoverflow: "In a simple call chain A->B->C->D, inside D msg.sender will be C, and tx.origin will be A."

What's next? 🤔

Remember that using low level calls function can lead to devastating results. That's why we need to use them consciously. So... let's go to the next post and see potential risks ⚡ and vulnerabilities ☠️.

Moreover knowledge of calls and delegatecalls is the key to understanding Upgradable Smart Contracts, which we discussed here (link) 😄🤗!