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 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.
If we know target contract ABI, we can directly use function signature
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)
If we don’t know target contract ABI, use call or delegatecall
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()
.
How EVM saves field variables to Storage
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
}
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.
How Smart Contract Function is Called
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.
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 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
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.
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.
Test case
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;
}
}
Test 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 saved into “Calculator” storage
- So “Calculator”
calculateResult
should be 3, anduser
address should set to "Machine" address. - And “Machine”
calculateResult
should be 0, anduser
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)
Test 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 saved into “Machine” storage
- So “Calculator”
calculateResult
should be 0, anduser
address should set to ZERO address. - And “Machine”
calculateResult
should be 3, anduser
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 failing1) 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.
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)
Wrap up
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()
, ordelegatecall()
. But in the case ofdelegatecall()
, we need to care about the order of the field variable.
Source code
If you want to test on your own, you can find the code on this repository.
Join Coinmonks Telegram Channel and Youtube Channel get daily Crypto News
Also, Read
- Crypto Telegram Signals | Crypto Trading Bot
- Copy Trading | Crypto Tax Software
- Grid Trading | Crypto Hardware Wallet
- Best Crypto Exchange | Best Crypto Exchange in India
- Best Crypto APIs for Developers
- Best Crypto Lending Platform
- An ultimate guide to Leveraged Token
- Best VPNs for Crypto Trading
- Best Crypto Analytics or On-Chain Data | Bexplus Review
- 10 Biggest NFT MarketPlaces to Mint a Collection
- AscendEx Staking | Bot Ocean Review | Best Bitcoin Wallets
- Bitget Review | Gemini vs BlockFi | OKEx Futures Trading