narrowing conversion of an out-of-range `constant` variable is not truncated, so failing assertions are proved safe
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 43/100
Research direction
Start in libsolidity/formal/SMTEncoder.cpp, at visitTypeConversion and the castSize < argSize branch named in the report. Compare how constant variables are handled with the narrowing conversion semantics described in the Solidity documentation, and use the supplied C.sol reproducer with the given SMTChecker command to inspect the result. Done means the checker reports violations for both assertions that fail on the EVM.
Written by the indexing model from the issue text.
Description
Found by comparing the semantics described in the Solidity documentation with how the Solidity SMTChecker implements them. An AI agent found this bug, reproduced it with solc 35b1cb80 (develop) and drafted this report. A second agent tried to refute it and searched upstream for duplicates. The methodology is described here.
What went wrong: Wrong result: CHC (and BMC) report "Assertion violation check is safe!" for assertions that fail on the EVM.
Version: 0.8.37+commit.f401782d (static linux binary). Also reproduces at develop 3eb0c3acb41f (0.8.38-develop.2026.10.20+commit.3eb0c3ac).
Preconditions: default SMTChecker settings; the program narrows a constant variable (or a constant expression over one) whose value does not fit the target type.
Input
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract C {
uint256 constant K = 300;
function f() public pure returns (uint8) {
uint8 r = uint8(K); // 44 on the EVM
assert(r != 44); // fails on the EVM
return r;
}
function g(uint256 x) public pure {
uint8 r = uint8(K);
r;
assert(x != 5); // fails for x = 5
}
}
Command
SOLC=/path/to/solc ./run.sh # exit 1 = bug reproduced, 0 = fixed, 2 = tool missing / harness error
solc --model-checker-engine chc --model-checker-targets assert --model-checker-show-proved-safe C.sol
Output
Info: CHC: Assertion violation check is safe!
--> C.sol:7:5:
|
7 | assert(r != 44); // fails on the EVM
| ^^^^^^^^^^^^^^^
Info: CHC: Assertion violation check is safe!
--> C.sol:13:5:
|
13 | assert(x != 5); // fails for x = 5
| ^^^^^^^^^^^^^^
Running the compiled runtime on geth 1.17.8 (EELS gives the same result), f() and g(5) both revert with Panic(0x01):
$ evm run --input 26121ff0 "$(solc --bin-runtime C.sol | tail -n 1)" # f()
$ evm run --input e420264a00…05 "$(solc --bin-runtime C.sol | tail -n 1)" # g(5)
{"tool": "geth", "fork": "osaka", "status": "revert", "returndata": "0x4e487b710000000000000000000000000000000000000000000000000000000000000001", "gas_used": 21253, "logs": [], "storage": {}, "error": "execution reverted"}
{"tool": "geth", "fork": "osaka", "status": "revert", "returndata": "0x4e487b710000000000000000000000000000000000000000000000000000000000000001", "gas_used": 21663, "logs": [], "storage": {}, "error": "execution reverted"}
Expected
Violations for both assertions. Per the docs (Types > Conversions between Elementary Types > Explicit Conversions): "If an integer is explicitly converted to a smaller type, higher-order bits are cut off", so uint8(K) is 44. docs/smtchecker.rst says a target reported safe is safe.
The modelled value is the untruncated 300, which makes the rest of the function infeasible. Because of that, assert(x != 5) is reported safe even though it never reads r. Using uint8(uint256(300)) or a non-constant argument gives the correct counterexample (r = 44). Location: libsolidity/formal/SMTEncoder.cpp visitTypeConversion, castSize < argSize branch (line 1236 at develop). It takes the isConstant() shortcut ("The TypeChecker guarantees that a constant fits in the cast size", from #11377). Since isConstant() also folds constant variables via ConstantEvaluator, the guarantee only holds for literals. #16653 / #16654 fixed the same isConstant pattern for unary operators only.
Reproducer files
C.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract C {
uint256 constant K = 300;
function f() public pure returns (uint8) {
uint8 r = uint8(K); // 44 on the EVM
assert(r != 44); // fails on the EVM
return r;
}
function g(uint256 x) public pure {
uint8 r = uint8(K);
r;
assert(x != 5); // fails for x = 5
}
}
oracle.sh
# Concrete EVM oracle: go-ethereum's `evm` tool (set EVM=/path/to/evm; default: from PATH).
# oracle RUNTIME_HEX [CALLDATA_HEX] [VALUE_WEI] -> one JSON line {status, error, returndata, storage, logs}; rc 2 on tool error.
EVM="${EVM:-evm}"
oracle() {
local o e rc g=(); o=$(mktemp /tmp/oracle-o.XXXXXX); e=$(mktemp /tmp/oracle-e.XXXXXX)
if [ "${3:-0}" != 0 ]; then # fund the sender so a value transfer is possible
g=(--prestate "$o.g" --sender 0x0000000000000000000000000000000000000001)
echo '{"config":{"chainId":1,"homesteadBlock":0,"eip150Block":0,"eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"berlinBlock":0,"londonBlock":0,"mergeNetsplitBlock":0,"shanghaiTime":0,"cancunTime":0,"pragueTime":0,"terminalTotalDifficulty":0,"blobSchedule":{"cancun":{"target":3,"max":6,"baseFeeUpdateFraction":3338477},"prague":{"target":6,"max":9,"baseFeeUpdateFraction":5007716}}},"alloc":{"0x0000000000000000000000000000000000000001":{"balance":"0xffffffffffffffff"}},"difficulty":"0x0","gasLimit":"0x1000000","timestamp":"0x0"}' > "$o.g"
fi
"$EVM" run --gas 10000000 --input "${2:-}" --value "${3:-0}" "${g[@]}" --dump --trace "$1" >"$o" 2>"$e"; rc=$?
[ $rc -eq 0 ] && python3 -I - "$o" "$e" <<'PY'
import json,sys
o,e=open(sys.argv[1]).read(),open(sys.argv[2]).read()
tr=[json.loads(l) for l in e.splitlines() if l.startswith('{')]
last=tr[-1]; err=last.get("error","")
logs=sum(1 for t in tr if str(t.get("opName","")).startswith("LOG"))
accts=json.loads(o[o.index('{'):]).get("accounts",{})
st={}
for a in accts.values(): st.update(a.get("storage",{}))
print(json.dumps({"status":"error" if err else "success","error":err,"returndata":"0x"+last.get("output",""),"storage":st,"logs":logs}))
PY
rc=$?; rm -f "$o" "$o.g" "$e"; [ $rc -eq 0 ] || echo "harness error: evm run failed" >&2; return $rc
}
run.sh
#!/usr/bin/env bash
# exit 1 = bug (CHC proves assertions safe that fail on the EVM), 0 = fixed, 2 = harness error
set -u
D="$(cd "$(dirname "$0")" && pwd)"
# Tools (override via env; defaults are looked up on PATH): SOLC, EVM (go-ethereum `evm`, the concrete oracle).
S="$(command -v "${SOLC:-solc}")" || { echo "harness error: solc not found (set SOLC)"; exit 2; }
source "$D/oracle.sh"
"$S" --version | tail -n 1
out="$("$S" --model-checker-engine chc --model-checker-targets assert --model-checker-show-proved-safe "$D/C.sol" 2>&1)" || { echo "harness error"; exit 2; }
echo "$out"; safe=$(echo "$out" | grep -c "Assertion violation check is safe")
code="$("$S" --bin-runtime "$D/C.sol" | tail -n 1)"
fails=0
for cd in 26121ff0 e420264a0000000000000000000000000000000000000000000000000000000000000005; do
for o in geth; do j="$(oracle "$code" "$cd")" || exit 2; echo "$o 0x$cd: $j"
echo "$j" | grep -q '4e487b710000000000000000000000000000000000000000000000000000000000000001' && fails=$((fails+1)); done
done
echo "CHC safe: $safe/2, EVM Panic(0x01): $fails/2"
[ $fails -eq 2 ] || exit 2
[ $safe -gt 0 ] && exit 1 || exit 0
If this is intended behaviour or a duplicate, feel free to close it, and I'll use that as feedback to improve future reports. For questions, or to have this re-checked on a newer version of the Solidity SMTChecker, mention @JeroenRobben or email [email protected].
- Dominant language
- C++
- Stars
- 25.8k
- Forks
- 6.2k
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 43
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from argotorg/solidity
-
AI tool policy: add AGENTS.mdMay be free again @cameel claimed this 47 days ago, and no pull request is open. Opendocumentation :book:
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
argotorg/solidity#16996 · 2 comments · 1 assignee ·
Maintainers usually reply within 1 day
-
`fitsPrecisionExp` charges the base's bit *length* per multiplication instead of its bit *position*, so valid constant powers such as `2 ** 2049` are rejectedPossibly taken @clonker claimed this 54 days ago. Openbug :bug:
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
argotorg/solidity#16984 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
`0 ** (-1)` is silently evaluated to `0` in a constant expression instead of being rejected as division by zeroMay be free again @matheusaaguiar claimed this 54 days ago, and no pull request is open. Openbug :bug: low impact
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
argotorg/solidity#16982 · 3 comments · 1 assignee ·
Maintainers usually reply within 1 day
-
AssemblyItem AssemblyItem::toSubAssemblyTag assumes size_t is at least 64bPossibly taken @riba2534 claimed this 67 days ago. Openbug :bug: low impact
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
FileReader: Duplicate search paths cause false ambiguous importsPossibly taken @zjuncal claimed this 2 days ago. Openbug :bug:
Difficulty 2/5 1-3 hours Newbie friendliness 35/100
Maintainers usually reply within 1 day
All issues in argotorg/solidity
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
google/tensorstore#306 ·
-
agent:WSL bug code-review linux pri:medium sev:low tooling
Difficulty 2/5 Half a day Newbie friendliness 62/100
Maintainers usually reply within 1 day