argotorg / argotorg/solidity

encodeCall with value-bound function ICEs legacy backend

Open
#16,790 1 comment 0 reactions 1 assignee Claimed by @msooseth View on GitHub
bug :bug: low impact
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.