DelegateCall: Calling Another Contract Function in Solidity

Photo by NOAA on Unsplash

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.

pragma solidity ^0.5.8;

contract Storage {
    uint public val;
    constructor(uint v) public {
        val = v;
    }
    function setValue(uint v) public {
        val = v;
    }
}

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

pragma solidity ^0.5.8;

import "./Storage.sol";

contract Machine {
    Storage public s;

    constructor(Storage addr) public {
        s = addr;
        calculateResult = 0;
    }
    
    function saveValue(uint x) public returns (bool) {
        s.setValue(x);
        return true;
    }
    function getValue() public view returns (uint) {
        return s.val();
    }
}

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.

const StorageFactory = artifacts.require('Storage');
const MachineFactory = artifacts.require('Machine');

contract('Machine', accounts => {
  const [owner, ...others] = accounts;

  beforeEach(async () => {
    Storage = await StorageFactory.new(new BN('0'));
    Machine = await MachineFactory.new(Storage.address);
  });

  describe('#saveValue()', () => {
    it('should successfully save value', async () => {
      await Machine.saveValue(new BN('54'));
      (await Storage.val()).should.be.bignumber.equal(new BN('54'));
    });
  });
});

And test passed!

Contract: Machine
  After initalize
    #saveValue()
      ✓ should successfully save value (56ms)

1 passing (56ms)

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.

contract Sample1 {
    uint256 first; 	// slot 0
    uint256 second;	// slot 1
}
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.

pragma solidity ^0.5.8;

import "./Storage.sol";

contract Machine {
    Storage public s;
    
    uint256 public calculateResult;
    
    address public user;
		
    event AddedValuesByDelegateCall(uint256 a, uint256 b, bool success);
    event AddedValuesByCall(uint256 a, uint256 b, bool success);
    
    constructor(Storage addr) public {
        ...
        calculateResult = 0;
    }
    
		...
    
    function addValuesWithDelegateCall(address calculator, uint256 a, uint256 b) public returns (uint256) {
        (bool success, bytes memory result) = calculator.delegatecall(abi.encodeWithSignature("add(uint256,uint256)", a, b));
        emit AddedValuesByDelegateCall(a, b, success);
        return abi.decode(result, (uint256));
    }
    
    function addValuesWithCall(address calculator, uint256 a, uint256 b) public returns (uint256) {
        (bool success, bytes memory result) = calculator.call(abi.encodeWithSignature("add(uint256,uint256)", a, b));
        emit AddedValuesByCall(a, b, success);
        return abi.decode(result, (uint256));
    }
}

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

pragma solidity ^0.5.8;

contract Calculator {
    uint256 public calculateResult;
    
    address public user;
    
    event Add(uint256 a, uint256 b);
    
    function add(uint256 a, uint256 b) public returns (uint256) {
        calculateResult = a + b;
        assert(calculateResult >= a);
        
        emit Add(a, b);
        user = msg.sender;
        
        return calculateResult;
    }
}

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.
describe('#addValuesWithCall()', () => {
  let Calculator;
      
  beforeEach(async () => {
    Calculator = await CalculatorFactory.new();
  });
      
  it('should successfully add values with call', async () => {
    const result = await Machine.addValuesWithCall(Calculator.address, new BN('1'), new BN('2'));

    expectEvent.inLogs(result.logs, 'AddedValuesByCall', {
      a: new BN('1'),
      b: new BN('2'),
      success: true,
    });

    (result.receipt.from).should.be.equal(owner.toString().toLowerCase());
    (result.receipt.to).should.be.equal(Machine.address.toString().toLowerCase());

    (await Calculator.calculateResult()).should.be.bignumber.equal(new BN('3'));
    (await Machine.calculateResult()).should.be.bignumber.equal(new BN('0'));

    (await Machine.user()).should.be.equal(constants.ZERO_ADDRESS);
    (await Calculator.user()).should.be.equal(Machine.address);
  });
});

And test pass as expected!

Contract: Machine
  After initalize
    #addValuesWithCall()
      ✓ should successfully add values with call (116ms)


1 passing (116ms)

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.
describe('#addValuesWithDelegateCall()', () => {
  let Calculator;
  
  beforeEach(async () => {
    Calculator = await CalculatorFactory.new();
  });
  
  it('should successfully add values with delegate call', async () => {
    const result = await Machine.addValuesWithDelegateCall(Calculator.address, new BN('1'), new BN('2'));

    expectEvent.inLogs(result.logs, 'AddedValuesByDelegateCall', {
      a: new BN('1'),
      b: new BN('2'),
      success: true,
    });

    (result.receipt.from).should.be.equal(owner.toString().toLowerCase());
    (result.receipt.to).should.be.equal(Machine.address.toString().toLowerCase());

    // Calculator storage DOES NOT CHANGE!
    (await Calculator.calculateResult()).should.be.bignumber.equal(new BN('0'));
    
    // Only calculateResult in Machine contract should be changed
    (await Machine.calculateResult()).should.be.bignumber.equal(new BN('3'));

    (await Machine.user()).should.be.equal(owner);
    (await Calculator.user()).should.be.equal(constants.ZERO_ADDRESS);
  });
});

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

0 passing (236ms)
1 failing

1) Contract: Machine
     After initalize
       #addValuesWithDelegateCall()
         should successfully add values with delegate call:

    AssertionError: expected '562046206989085878832492993516240920558397288279' to equal '3'
    + expected - actual

    -562046206989085878832492993516240920558397288279
    +3

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.

contract Machine {
    uint256 public calculateResult;
    
    address public user;
    
    Storage public s;
    
    ...
}

And finally test passed!

Contract: Machine
  After initalize
    #addValuesWithDelegateCall()
      ✓ should successfully add values with delegate call (106ms)


1 passing (247ms)

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