super can dispatch a pure/view function to a state-modifying implementation
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 29
Description
A `super.f()` call is type-checked against the candidate set of the contract
that lexically contains it, but resolved at code generation time over the
linearization of the most derived contract. Those are two different contract
lists, and state mutability is not among the properties re-checked during
resolution.
The result is that a function declared `pure` can execute an `SSTORE`. The
compiler reports no error and no warning, and the ABI still advertises the
function as `pure`.
## Reproducer
```solidity
contract A {
function f() public pure virtual returns (uint256) { return 1; }
}
contract X {
uint256 public s;
function f() public virtual returns (uint256) { s = 5; return 100; }
}
contract B is A {
function f() public pure virtual override returns (uint256) { return super.f(); }
}
contract D is A, X, B {
function f() public pure override(A, X, B) returns (uint256) { return super.f(); }
}
```
Compiles without an issue. Deploying `D` and calling it gives:
```
f() -> 100 // X.f ran, not A.f
s() -> 5 // a pure function wrote storage
```
`solc --abi` reports `"stateMutability": "pure"` for `D.f`.
With `view` instead of `pure` throughout, the same construction produces a
contract that cannot be used at all from a static context, since `view` is the
half of the guarantee the EVM enforces:
```solidity
contract Caller {
D d = new D();
function g() public view returns (uint256) { return d.f(); }
}
```
`D.f` is declared `view`, so the call compiles to `STATICCALL`, the `super`
chain lands on the storage-writing `X.f`, and `g()` reverts. Every external
view call to `D.f()` fails.
## Expected behaviour
A compile-time error on the joining contract.
Contributor guide
Assessment
This issue has not been assessed yet.