I am trying to determine the behavior of call
when executed from a constant function.
Of course, the compiler issues a warning on that:
Warning: Function declared as view, but this expression (potentially) modifies the state and thus requires non-payable (the default) or payable
But since this is not an error, I can move forward and see what happens.
Please note that I'm on Solc 0.4.25; I'm pretty sure that things have changed on Solc v0.5.x.
Anyway, I went ahead and implemented a pair of small contracts in order to test this:
pragma solidity 0.4.25;
contract Callee {
uint256 public xxxx;
function func() external returns (uint256) {
xxxx = 123;
return 456;
}
}
contract Caller {
Callee private callee = new Callee();
uint256 public yyyy;
function callFromConstantFunc() external view returns (uint256) {
uint256[1] memory retv;
address dest = address(callee);
bytes memory selector = abi.encodeWithSelector(callee.func.selector);
assembly {
let status := call(gas, dest, 0, add(selector, 32), mload(selector), retv, 32)
if iszero(status) {
revert(0, 0)
}
}
return retv[0];
}
function callFromNonConstantFunc() external returns (uint256) {
uint256[1] memory retv;
address dest = address(callee);
bytes memory selector = abi.encodeWithSelector(callee.func.selector);
assembly {
let status := call(gas, dest, 0, add(selector, 32), mload(selector), retv, 32)
if iszero(status) {
revert(0, 0)
}
}
yyyy = retv[0];
}
function xxxx() external view returns (uint256) {
return callee.xxxx();
}
}
Then I tested both methods via Truffle:
contract("Test", function(accounts) {
it("callFromConstantFunc", async function() {
const caller = await artifacts.require("Caller").new();
const retv = await caller.callFromConstantFunc();
const xxxx = await caller.xxxx();
console.log(`retv = ${retv}, xxxx = ${xxxx}`);
});
it("callFromNonConstantFunc", async function() {
const caller = await artifacts.require("Caller").new();
await caller.callFromNonConstantFunc();
const retv = await caller.yyyy();
const xxxx = await caller.xxxx();
console.log(`retv = ${retv}, xxxx = ${xxxx}`);
});
});
The results are rather staggering IMO:
callFromConstantFunc: retv = 456, xxxx = 0
callFromNonConstantFunc: retv = 456, xxxx = 123
As you can see, when I invoked Callee.func
from Caller.callFromConstantFunc
, the transaction has completed without reverting, but the value of Callee.xxxx
has remained unchanged.
For the record, I have also invoked Callee.func
from Caller.callFromNonConstantFunc
, where the value of Callee.xxxx
has changed as expected.
Of course, changing call
to staticcall
would cause both Caller
functions to revert, because the Callee
function itself is not constant.
However, my question focuses on the behavior of call
, and I'm trying to figure out how it is possible for a transaction to complete successfully and yet - without "doing the job".
Some clarification on the parameters that I pass to call
:
gas
- the remaining amount of gas for the transactiondest
- the address of theCallee
instance0
- the amount of wei passed to the functionadd(selector, 32)
- the start address of the input (see explanation below)mload(selector)
- the length of the input (see explanation below)retv
- the start address of the output32
- the length of the output
Because selector
is a bytes
array, the first 32 bytes contain the length of this array, and the actual data starts immediately after.
I have executed the test via Ganache.
Can someone please confirm that this is the expected behavior if I deploy such contract to a "real" network? Also, how is it even possible for this kind of behavior to be "allowed" on Ethereum?
Thank you.
callFromConstantFunc
doesn't create a transaction, so there's no way for it to modify the internal state of the contract. Calling a non constant function outside a contract is useful if you want to simulate behavior without spending ether. I've previously asked a similar question that remains unanswered ethereum.stackexchange.com/questions/28040/…