encodeCall with value-bound function ICEs legacy backend
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 29
Description
## Description
Passing a value-bound or gas-bound external function pointer as the first argument to `abi.encodeCall` triggers an Internal Compiler Error in the legacy codegen backend:
```
Internal compiler error:
/solidity/libsolidity/codegen/ExpressionCompiler.cpp(1505): Throw in function
virtual bool solidity::frontend::ExpressionCompiler::visit(const FunctionCall&)
Dynamic exception type: boost::wrapexcept
std::exception::what: Solidity assertion failed
```
The failing assertion is:
```cpp
// libsolidity/codegen/ExpressionCompiler.cpp:1495-1518 — ABIEncodeCall codegen
{
auto const& funType = dynamic_cast(*selectorType);
if (funType.kind() == FunctionType::Kind::Declaration)
{
solAssert(funType.hasDeclaration());
solAssert(selectorType->sizeOnStack() == 0);
m_context << funType.externalIdentifier();
}
else
{
solAssert(selectorType->sizeOnStack() == 2); // ← LINE 1505 — ICE site
// stack:
// Extract selector from the stack
m_context << Instruction::SWAP1 << Instruction::POP;
}
// ...
}
```
The `else` branch assumes the external function pointer occupies exactly 2 stack slots (address + selector). However, `FunctionType::sizeOnStack()` returns:
- 2 for plain external functions (addr + selector)
- 3 for `{value: ...}`-bound or `{gas: ...}`-bound functions (addr + selector + value/gas)
- 4 for both value- and gas-bound
Any `{...}`-bound function passed to `abi.encodeCall` therefore trips the assertion.
The same source compiles cleanly under `--via-ir`, because `IRGeneratorForStatements` uses a different codegen path that handles all `sizeOnStack` values correctly. Semantically, `encodeCall` only emits selector + args, so the value/gas binding should simply be dropped during encoding.
The TypeChecker accepts the value-bound function pointer as a valid first argument to `abi.encodeCall` (it is rejected in nearly every other context — parameter passing, function returns, storage assignment, equality comparison — with various TypeErrors), so `abi.encodeCall` appears to be the sole front-end-accepted path that reaches the failing legacy codegen invariant.
Expected behavior: legacy backend should either accept value/gas-bound function pointers in `abi.encodeCall` (dropping the binding, matching via-IR), or the TypeChecker should reject them at the call site. It should not ICE.
## Environment
- Compiler version: 0.8.35-develop.2026.5.5+commit.47b9dedd.Linux.g++
- Compilation pipeline (legacy, IR, EOF): legacy (ICE); via-IR compiles cleanly
- Operating system: Linux Ubuntu Jammy
## Steps to Reproduce
Minimal reproducer:
```solidity
contract C {
function g() external payable {}
function f() public view returns (bytes memory) {
return abi.encodeCall(this.g{value: 100}, ());
}
}
```
Compile with the legacy backend (default):
```
$ solc --bin
Internal compiler error:
/solidity/libsolidity/codegen/ExpressionCompiler.cpp(1505): Throw in function
virtual bool solidity::frontend::ExpressionCompiler::visit(const FunctionCall&)
Dynamic exception type: boost::wrapexcept
std::exception::what: Solidity assertion failed
```
The same source under `--via-ir` produces clean bytecode:
```
$ solc --bin --via-ir
... clean bytecode output ...
```
Both `{value: X}` and `{gas: X}` (and combinations) trip the same assertion:
```solidity
abi.encodeCall(this.g{value: 100}, ()) // ICE
abi.encodeCall(this.g{gas: 100}, ()) // ICE
abi.encodeCall(this.g{value: 1, gas: 2}, ()) // ICE
```
Contributor guide
Assessment
This issue has not been assessed yet.