1

I'm trying to create a modifier that only allows a function to be called using staticcall. Using the view declaration in Solidity 0.5.0 guarantees that a function won't change state, but it still allows the function to be invoked with call from external sources. It would be interesting to be able to block all potentially-insecure calling methods directly.

My current solution tests for a staticcall by using a standard call into a function that will emit an event, and only allowing the transaction to proceed if that call fails. It exposes this feature via an onlyStaticCall modifier. There are a few issues with this approach:

  • Since the check uses a call, it will not allow for the modified function to also be declared as a view function. It would obviously be preferable for the two modifiers to coexist, although the onlyStaticCall modifier might make view redundant.
  • While the modifier seems to work, it makes a potentially superfluous external call to itself. A more efficient and elegant solution would be able to "try" to emit a log without needing to invoke call, or even introspect the calling method directly.
  • I'm not sure whether or not a delegatecall could circumvent this restriction.
  • Would there also be a way to enable web3 message "call" transactions (vs. regular "send" transactions) to succeed on the modified function? (The terminology here is a bit confusing, as the call opcode is for state-changing calls but web3 "call" is for read-only contract access.)

Any ideas on how to address these issues?

Existing Code:

pragma solidity ^0.5.0;


contract StaticCallChecker {
  // an event that signifies that the state is no longer unchanged
  event StateUnchanged(bool);

  // a function that changes state by emitting an (empty) event
  function changeState() public {
    emit StateUnchanged(false);
  }

  // example function that can only be called using staticcall
  // NOTE: making a "call" from web3 (vs. a "send") doesn't use a staticcall!
  function test() public onlyStaticCall returns (bool) {
    return true;
  }

  // make a staticcall to `test()` internally, should always return true
  function testStaticCall() external view returns (bool result) {
    // perform the staticcall and get success status and returned data
    (bool success, bytes memory data) = address(this).staticcall(
      abi.encodePacked(
        this.test.selector
      )
    );

    // if the staticcall was successful, load return bytes as a boolean
    if (success) {
      assembly {
        result := mload(add(data, 0x20))
      }
    }
  }

  // modifier that will only allow the caller to use staticcall
  modifier onlyStaticCall() {
    require(_isStaticCall());
    _;
  }

  // an internal function for checking if a caller is using staticcall
  function _isStaticCall() internal returns (bool) {
    // perform a call and get success status (ignore return data)
    (bool success, ) = address(this).call(
      abi.encodePacked(
        this.changeState.selector
      )
    );

    // the call should fail in order for the original call to be a staticcall
    return !success;
  }
}

Test output:

running tests...
 ✓ contract deploys successfully
 ✓ contract cannot change state via web3 "call"
 ✓ contract can change state via web3 "send"
 ✓ test reverts via web3 "call"
 ✓ test reverts via web3 "send"
 ✓ testStaticCall returns true via web3 "call"
 ✓ testStaticCall does not revert and does not log an event via web3 "send"
completed 7 tests with 0 failures.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.