2

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 transaction
  • dest - the address of the Callee instance
  • 0 - the amount of wei passed to the function
  • add(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 output
  • 32 - 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.

2
  • The call to 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/…
    – Ismael
    Jun 26, 2019 at 20:56
  • @Ismael: Thank you. I'd expect a revert in this case, but the function call completes successfully. Jun 27, 2019 at 4:57

1 Answer 1

0

From the Solidity documentation:

The compiler does not enforce yet that a view method is not modifying state. It raises a warning though.

Declaring a method as view will indicate in the ABI that the method is intended to be accessed via a call rather than a transaction, but this behavior is in no way enforced. eth_call executes a transaction locally without broadcasting it and so without actually modifying the state. As @Ismael mentioned in the comments, this is useful for simulating a transaction to ensure it will not revert without actually spending any ether.

For your example, if you execute Caller.callFromConstantFunc() as a transaction rather than a call it will modify the state of xxxx in Callee:

>>> caller = accounts[0].deploy(Caller)
Transaction sent: 0xa63c8d2e484c64b92e3a99e67ba2edf0fa7026a6b7b7d753c7c4e0956edf7831
Caller.constructor confirmed - block: 1   gas used: 367205 (5.48%)
Caller deployed at: 0x5f7Cc61aDD8Cf64583f14993529AFe393d0BD9A8

>>> caller.callFromConstantFunc()  # executes as a call, state will not change
456
>>> caller.xxxx()
0

>>> tx = caller.callFromConstantFunc.transact({'from': accounts[0]})
Transaction sent: 0xf8c1b8e22650f90ed929f2b55aad2c50c878c9fb48d58d3587baf9b2f1d8bc03
Caller.callFromConstantFunc confirmed - block: 2   gas used: 42886 (0.64%)
>>> tx.return_value
456
>>> caller.xxxx()
123

When writing a contract that does modify state in a view function, you must always consider that someone can call it as a transaction and so any change it makes can be permanent.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.