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 aview
function. It would obviously be preferable for the two modifiers to coexist, although theonlyStaticCall
modifier might makeview
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.