Photo by NOAA on Unsplash

DelegateCall: Calling Another Contract Function in Solidity

In this post, we’re going to see how we can call another contract function. And we talk about more deeply about delegatecall

When writing Ethereum Smart Contract code, there are some cases when we need to interact with other contract. In Solidity, for this purpose, there’s several ways to achieve this goal.

If we know target contract ABI, we can directly use function signature

Let’s say we have deployed simple contract called “Storage” that allows user to save a value.

And we want to deploy another contract called “Machine” which is caller of “Storage” contract. “Machine” reference “Storage” contract and change its value.

In this case, we know the ABI of “Storage” and its address, so that we can initialise existing “Storage” contract with the address and ABI tells us how we can call “Storage” contract’s function. We can see “Machine” contract call “Storage” setValue() function.

And write a test code to check whether “Machine” saveValue() function actually calls “Storage” setValue() function and change its state.

And test passed!

If we don’t know target contract ABI, use call or delegatecall

But what if caller (in this case “Machine” contract) doesn’t know the ABI of target contract?

Still we can call target contract’s function with call() and delegatecall().

Before explain about Ethereum Solidity call() and delegatecall(), it would be helpful to see how EVM saves contract’s variables to understand call() and delegatecall().

How EVM saves field variables to Storage

In Ethereum, there’s two kinds of space for saving contract’s field variables. One is “memory” and the other is “storage”. And what ‘foo is saved to storage’ means that the value of ‘foo’ is permanently recorded to states.

Then how could so many variables in single contract not overlap each other’s address space? EVM assign slot number to the field variables.

EVM saves field variables using slot
EVM saves field variables using slot

Because first is declared first in “Sample1” it is assigned 0 slot. Each different variables distinguished by its slot number.

In EVM it has 2256 slot in Smart Contract storage and each slot can save 32byte size data.

How Smart Contract Function is Called

Like general programming code such as Java, Python, Solidity function can be seen as group of commands. When we say ‘function is called’, it means that we inject specific context (like arguments) to that group of commands (function) and commands are executed one by one with this context.

And function, group of commands, address space can be found by its name.

In Ethereum function call can be expressed by bytecode as long as 4 + 32 * N bytes. And this bytecode consist of two parts.

  • Function Selector: This is first 4 bytes of function call’s bytecode. This is generated by hashing target function’s name plus with its arguments type excluding empty space. For example savaValue(uint). Currently Ethereum use keccak-256 hashing function to create function selector. Based on this function selector, EVM can decide which function should be called in the contract.
  • Function Argument: Convert each value of arguments into hex string with the fixed length of 32bytes. If there is more than one argument, concatenate

If user pass this 4 + 32 * N bytes bytecode to the data field of transaction. EVM can find which function should be executed then inject arguments to that function.

Explain DelegateCall with test case

Context

There’s a word “context” when we talked about how smart contract function is called. Actually the word “context” is much general concept in software and the meaning is changed little bit depending on the context.

When we talked about execution of program, we can say “context” as all the environment like variable or states at the point of execution. For example, on the point of exeuction of program ‘A’, the username who execute that program is ‘zeroFruit’, then username ‘zeroFruit’ can be context of program ‘A’.

In the Ethereum Smart Contract, there is lots of context and one representative thing is ‘who execute this contract’. You may be seen msg.sender a lot in Solidity code and the value of msg.sender address vary depending on who execute this contract function.

DelegateCall

DelegateCall, as name implies, is calling mechanism of how caller contract calls target contract function but when target contract exeucted its logic, the context is not on the user who execute caller contract but on caller contract.

Context when contract call another contract
Context when contract calls another contract
Context when contract delegatecall another contract
Context when contract delegatecalls another contract

Then when contract delegatecall to target, how state of storage would be changed?

Because when delegatecall to target, the context is on Caller contract, all state change logics reflect on Caller’s storage.

For example, let’s there’s Proxy contract and Business contract. Proxy contract delegatecall to Business contract function. If user call Proxy contract, Proxy contract will delegatecall to Business contract and function would be executed. But all state changes will be reflect to Proxy contract storage, not Business contract.

Test case

This is extension version of contract explained before. It still has “Storage” as field and addValuesWithDelegateCall , addValuesWithCall in addition to test how storage would be changed. And “Machine” has calculateResult , user for saving add result and who called this function each.

And this is our target contract “Calculator”. It also has calculateResult and user.

addValuesWithCall

And this is our addValuesWithCall test code. What we need to test is

  • Because context is on “Calculator” not “Machine”, add result should be save into “Calculator” storage
    • So “Calculator” calculateResult should be 3, and user address should set to “Machine” address.
    • And “Machine” calculateResult should be 0, and user to ZERO address.

And test pass as expected!

addValuesWithDelegateCall

And this is our addValuesWithCall test code. What we need to test is

  • Because context is on “Machine” not “Calculator”, add result should be save into “Calculator” storage
    • So “Calculator” calculateResult should be 0, and user address should set to ZERO address.
    • And “Machine” calculateResult should be 3, and user to EOA.

But FAILED! What??? Where ‘562046206989085878832492993516240920558397288279’ come from?

As we mentioned before each field variable has its own slot. And when we delegatecall “Calculator”, the context is on “Machine”, but the slot number is based on “Calculator”. So because “Calculator” logic override Storage address with calculateResult, so as calculateResult to user, test failed.

Based on this knowledge, we can find where ‘562046206989085878832492993516240920558397288279’ come from. It is decimal version of EOA.

"Machine" contract field variable overrided when delegatecall
“Machine” contract field variable overrided

So to fix this problem, we need to change the order of “Machine” field variable.

And finally test passed!

Wrap up

In this post, we’ve seen how we can call another contract’s function from contract.

  • If we know the ABI of target function, we can directly use target function signature
  • If we don’t know the ABI of target function, we can use call(), or delegatecall(). But in the case of delegatecall(), we need to care about the order of field variable.

Source code

If you want to test on your own, your can find the code on this repository.

https://github.com/zeroFruit/upgradable-contract/tree/feat/delegatecall