DelegateCall: Calling Another Contract Function in Solidity

zeroFruit
zeroFruit
Sep 1, 2019 · 8 min read
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 SmartContract code, there are some cases when we need to interact with other contracts. In Solidity, for this purpose, there are several ways to achieve this goal.

Let’s say we have deployed a simple contract called “Storage” that allows the 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 the 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 initialize existing “Storage” contract with the address and ABI tells us how we can call “Storage” contract’s function. We can see the “Machine” contract call “Storage” setValue() function.

And write test code to check whether “Machine” saveValue() 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 the test passed!

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

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

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

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

In Ethereum, there are two kinds of space for saving the 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 a 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

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

In EVM it has 2²⁵⁶ slot in Smart Contract storage and each slot can save 32-byte size data.

Like general programming code such as Java, Python, Solidity function can be seen as a 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 consists 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 the type of its arguments excluding empty space. For example savaValue(uint). Currently, Ethereum uses 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 a hex string with the fixed length of 32bytes. If there is more than one argument, concatenate

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

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 a little bit depending on the context.

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

In the Ethereum Smart Contract, there are lots of contexts 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 executes this contract function.

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

Context when the contract calls another contract
Context when contract delegatecall another contract

Then when contract delegatecall to target, how the 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 the user calls Proxy contract, Proxy contract will delegatecall to Business contract and function would be executed. But all state changes will be reflected Proxy contract storage, not a Business contract.

This is the extended version of the 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;
}
}

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

  • Because context is on “Calculator” not “Machine”, add result should be saved 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)

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

  • Because context is on “Machine” not “Calculator”, add result should be saved into “Machine” 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, the test failed.

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

“Calculator” contract field variable overrides “Machine” contract field variable

So to fix this problem, we need to change the order of the “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)

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

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

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

Get Best Software Deals Directly In Your Inbox

Coinmonks

Coinmonks is a non-profit Crypto educational publication.

Coinmonks

Coinmonks is a non-profit Crypto educational publication. Follow us on Twitter @coinmonks Our other project — https://coincodecap.com

zeroFruit

Written by

zeroFruit

Software/Blockchain Engineer, https://github.com/zeroFruit

Coinmonks

Coinmonks is a non-profit Crypto educational publication. Follow us on Twitter @coinmonks Our other project — https://coincodecap.com