The new Yul optimizer toolkit (#519)

This PR introduces powerful code size optimizations. This is achieved
with an additional IR layer in between Yul and LLVM IR. It works quite
well (see below for a comparison) because it captures optimization gains
that neither solc nor LLVM can realize.

The feature is gated behind `--newyork` or a corresponding standard JSON
input setting and **off by default**.

### How it works

We implemented an SSA IR modelled after or inspired by the [MLIR SCF
dialect](https://mlir.llvm.org/docs/Dialects/SCFDialect/). Yul is
translated into this IR and the IR implements mainly the following
optimizations:

- **Type narrowing**: narrows 256-bit values to I1..I160 where provable
(bidirectional + interprocedural), so LLVM emits native 32/64-bit ops
instead
    of software 256-bit arithmetic.
- **FMP range proof**: exposes the `mload(0x40) < heap_size` bound to
LLVM
IPSCCP, eliminating trailing overflow checks on FMP-derived offsets.
Single
    largest code-size contributor.
- **Heap analysis**: skips big-endian byte-swaps where native
little-endian
    layout is safe.
  - **mem_opt / keccak fusion+folding**, **compound outlining** (mapping
sload/sstore), **guard narrowing**, **inlining** tuned for PolkaVM, and
    **exact + fuzzy function dedup**.

### Results
The optimizations implemented with the newyork IR bring substantial code
size improvements.
  - Integration contracts: approximately **-20% to -50%** code size.
  - OpenZeppelin Wizard corpus: around **-47%** code size.

This table shows the code size improvements on various OpenZeppelin ERC
implementations, taken from their [contracts
wizard](https://wizard.openzeppelin.com/), with almost all features
enabled.

| Contract | Default: (bytes) | With `--newyork`: (bytes) | Reduction |
|---|---:|---:|---:|
| erc1155 | 59,206 | 30,649 | -48.2% |
| erc20 | 81,697 | 42,893 | -47.5% |
| erc721 | 92,052 | 49,423 | -46.3% |
| oz_gov | 141,861 | 81,159 | -42.8% |
| oz_rwa | 77,274 | 37,975 | -50.9% |
| oz_simple_erc20 | 29,950 | 16,554 | -44.7% |
| oz_stable | 82,117 | 39,032 | -52.5% |
| **Total** | **564,157** | **297,685** | **-47.2%** |

We believe this to be a reasonably representative dataset and conclude
that the newyork IR achieves drastic code size reductions on real world
dApp implementations.

### Methodology, security and runtime performance

Since the main contributors to these improvements are the FMP range
proofs and the type narrowing, we don't expect big runtime performance
regressions (it may even benefit runtime performance in some cases).

The PR also includes docs
(`book/src/developer_guide/newyork_{optimizer,ir}.md`) and many new
integration tests, including regression tests for known FMP soundness
traps.

The PR was authored collaboratively and reviewed extensively by the
`revive` maintainers, as well as Claude Opus, Claude Fable, Qwen,
Minimax and Deepseek LLMs, over a span of many months.

The `newyork` compiler pipeline introduces no new regressions over our
integration and differential test suites (containing over 30000 test
cases). This was achieved by careful manual reviews and many LLM bughunt
loops. Additionally, a final security review by Anthropic's Fable 5 LLM
found no remaining soundness issues. Nonetheless, like any brand new
software, please consider this an experimental feature upon release.

---------

Signed-off-by: xermicus <bigcyrill@hotmail.com>
Signed-off-by: xermicus <cyrill@parity.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: neo <me@90f9.org>
Co-authored-by: LJ <81748770+elle-j@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-06-17 12:35:16 +02:00
1 parent d178110a81
commit 98d818f958
183 files changed
+49503 -561

No files matched your search

+3
View File
@@ -7,6 +7,9 @@ repository.workspace = true
authors.workspace = true
description = "revive compiler integration test cases"
[features]
newyork = ["resolc/newyork"]
[dependencies]
alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true }
+8 -8
View File
@@ -1,10 +1,10 @@
{
"Baseline": 870,
"Computation": 2418,
"DivisionArithmetics": 9368,
"ERC20": 17090,
"Events": 1662,
"FibonacciIterative": 1427,
"Flipper": 2240,
"SHA1": 8056
"Baseline": 838,
"Computation": 2368,
"DivisionArithmetics": 11444,
"ERC20": 18057,
"Events": 1614,
"FibonacciIterative": 1373,
"Flipper": 2205,
"SHA1": 7830
}
+10
View File
@@ -0,0 +1,10 @@
{
"Baseline": 493,
"Computation": 1217,
"DivisionArithmetics": 7370,
"ERC20": 8726,
"Events": 909,
"FibonacciIterative": 969,
"Flipper": 1058,
"SHA1": 6264
}
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract ArrayProbe {
uint256[8] fixedArr;
uint256[] dynArr;
mapping(uint256 => uint256) m;
function run(uint256 a, uint256 b, uint256 c) public returns (uint256) {
for (uint256 i = 0; i < 8; i++) { fixedArr[i] = i * a + 1; }
// attacker-controlled index: out-of-bounds must revert (panic 0x32) identically
uint256 r = fixedArr[a & 7];
r = r + fixedArr[b & 7];
// dynamic array push/index
uint256 n = c & 15;
for (uint256 i = 0; i < n; i++) { dynArr.push(i + a); }
if (dynArr.length > 0) { r = r ^ dynArr[b % dynArr.length]; }
// mapping
m[a] = b;
m[b] = c;
r = r + m[a] + m[b];
// explicit bounds hit (reverts when a >= 8)
r = r + fixedArr[a];
return r;
}
}
@@ -0,0 +1,17 @@
/// A copy with a STATIC destination in [0x40, 0x60) but a DYNAMIC length
/// clobbers the free-memory-pointer slot with arbitrary bytes. A subsequent
/// mload(0x40) must observe the full 256-bit word (EVM), but newyork applies
/// its FMP range proof and truncates it because `fmp_could_be_unbounded` is
/// not set for the dynamic-length copy case (only the constant-length case).
object "CalldataCopyFmp" {
code { datacopy(0, dataoffset("CalldataCopyFmp_deployed"), datasize("CalldataCopyFmp_deployed")) return(0, datasize("CalldataCopyFmp_deployed")) }
object "CalldataCopyFmp_deployed" {
code {
let len := calldataload(0)
calldatacopy(0x40, 32, len)
let r := mload(0x40)
mstore(0, r)
return(0, 32)
}
}
}
@@ -0,0 +1,30 @@
/// Soundness PoC (newyork-specific): the `offx` parameter of `probe` is used as
/// an mload offset (case 0) which makes newyork narrow it to i64, and as a
/// `calldatacopy` SOURCE offset (case 1). newyork's CallDataCopy lowering treats
/// the calldata source offset as a heap pointer (`narrow_offset_for_pointer`),
/// so a large source offset traps on the PVM heap-bounds check / mis-sized copy
/// instead of zero-filling like EVM. 4 call sites keep `probe` non-inlined.
/// Reproduces only under RESOLC_USE_NEWYORK=1 (the Yul path is correct).
object "CalldataCopySrcNarrow" {
code { let s := datasize("CalldataCopySrcNarrow_deployed") codecopy(0, dataoffset("CalldataCopySrcNarrow_deployed"), s) return(0, s) }
object "CalldataCopySrcNarrow_deployed" {
code {
let op := calldataload(0)
let off := calldataload(32)
let r0 := probe(op, off)
let r1 := probe(op, off)
let r2 := probe(op, off)
let r3 := probe(op, off)
mstore(0x200, r0)
mstore(0x220, add(r1, add(r2, r3)))
return(0x200, 64)
function probe(opx, offx) -> r {
switch opx
case 0 { r := mload(offx) }
case 1 { calldatacopy(0x80, offx, 32) r := mload(0x80) }
case 2 { r := keccak256(offx, 32) }
default { r := offx }
}
}
}
}
@@ -0,0 +1,11 @@
object "CalldataloadOOB" {
code { datacopy(0, dataoffset("CalldataloadOOB_deployed"), datasize("CalldataloadOOB_deployed")) return(0, datasize("CalldataloadOOB_deployed")) }
object "CalldataloadOOB_deployed" {
code {
let off := calldataload(0) // the offset to test (from calldata[0])
let x := calldataload(off) // EVM: zero-pads beyond calldatasize
mstore(0, x)
return(0, 32)
}
}
}
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// Differential guard against a latent soundness footgun in the codegen for
// __revive_caller: before the fix the helper wrote its result through the
// shared @__address_spill_buffer LLVM global while declaring
// memory(inaccessiblemem: read). The same global is also written by the
// inlined tx.origin and address(this) patterns, so the function attribute
// was a contract violation from LLVM's point of view (globals are Other
// memory, not inaccessiblemem). Any optimizer pass that exploited the wrong
// attribute to CSE / hoist a load of the spill buffer across a
// __revive_caller() call would have miscompiled the surrounding tx.origin /
// address(this) read.
//
// The fix moves __revive_caller's output to a function-local alloca so the
// attribute matches the body. This contract exercises every shape that
// previously shared the spill buffer.
contract CallerOriginAliasing {
function caller_then_origin() external view returns (address, address) {
return (msg.sender, tx.origin);
}
function origin_then_caller() external view returns (address, address) {
return (tx.origin, msg.sender);
}
function caller_address_origin() external view returns (address, address, address) {
return (msg.sender, address(this), tx.origin);
}
function repeated_caller() external view returns (address, address, address) {
return (msg.sender, msg.sender, msg.sender);
}
}
@@ -0,0 +1,23 @@
object "CallvalueRevertData" {
code { let s := datasize("CallvalueRevertData_deployed") codecopy(0, dataoffset("CallvalueRevertData_deployed"), s) return(0, s) }
object "CallvalueRevertData_deployed" {
code {
let sel := calldataload(0)
// One callvalue read per case (separate regions, so CSE keeps them distinct): three reads
// enable the callvalue-check outline.
switch sel
case 1 { if callvalue() { revert(0, 0) } sstore(0, 0x11) }
case 2 { if callvalue() { revert(0, 0) } sstore(0, 0x22) }
case 3 {
// A data-carrying revert whose operands are bound in the enclosing (case) scope. The outline
// must not treat this as an empty revert and drop the returned memory[off, off+len).
mstore(0x80, 0xdeadbeef)
let off := calldataload(32)
let len := calldataload(64)
if callvalue() { revert(off, len) }
sstore(0, 0x33)
}
default { sstore(0, 0xff) }
}
}
}
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: `to_llvm.rs::get_or_create_return_block` (and the
/// emit-revert sibling) call `context.xlen_type().const_int(const_offset,
/// false)` to build the constant offset/length for the
/// `emit_exit_unchecked` path. `const_int` takes a `u64` and truncates
/// silently to `xlen_type`'s width (`i32`), so any constant
/// offset/length above `u32::MAX` (but still within `u64::MAX` so
/// `try_extract_const_u64` returns `Some`) is wrapped mod `2^32` and the
/// resulting return/revert reads from / aliases the truncated heap
/// offset. EVM expands memory and OOGs.
contract ConstReturnOverflowBug {
function bug() external pure returns (uint256 r) {
assembly {
// Constant offset 2^56 — fits in u64, doesn't fit in u32. With
// the current codegen, the shared `return_shared_…` block
// truncates to i32 (=0) and reads/writes at heap[0..32]
// instead of trapping.
mstore(0x100000000000000, mload(0x100000000000000))
return(0x100000000000000, 32)
// `r` is just to satisfy the signature; control flow exits
// via the inline-assembly `return` above.
r := 0
}
}
}
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: `mem_opt::optimize_statements` `*Copy` handler
/// (CodeCopy / ExtCodeCopy / ReturnDataCopy / DataCopy /
/// CallDataCopy) only invalidates `memory_state[word(dest)]` when
/// `dest` is statically known. It ignores the *length* entirely, so
/// additional words inside `[dest, dest + length)` keep their stale
/// tracked entries. A subsequent `mload` at the exact tracked offset
/// is forwarded to the pre-overwrite value while EVM reads the
/// actually-overwritten bytes from the copy.
///
/// The dynamic `length` parameter defeats solc's dead-store
/// elimination of the sentinel — solc can't prove that
/// `calldatacopy(0xc0, 0, length)` overwrites bytes `0xe0..0x100`
/// without knowing `length >= 0x40`.
contract CopyOverlapBug {
function bug(uint256 length) external pure returns (bytes32 r) {
assembly {
mstore(0xe0, 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)
calldatacopy(0xc0, 0, length)
r := mload(0xe0)
}
}
}
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Probe: custom error revert data must match EVM (selector + ABI-encoded args)
/// for edge-case argument values. Targets newyork's custom-error outlining.
contract CustomErrorArgs {
error E1(uint256 a);
error E2(uint256 a, uint256 b, address c);
error E3(uint8 x, bytes32 y);
function f(uint256 sel, uint256 a, uint256 b) external pure {
if (sel == 0) revert E1(a);
if (sel == 1) revert E2(a, b, address(uint160(a)));
if (sel == 2) revert E3(uint8(a), bytes32(b));
revert E1(~a);
}
}
@@ -0,0 +1,14 @@
object "CustomErrorDupArg" {
code { let s := datasize("CustomErrorDupArg_deployed") codecopy(0, dataoffset("CustomErrorDupArg_deployed"), s) return(0, s) }
object "CustomErrorDupArg_deployed" {
code {
// Custom-error revert shape (selector + one uint256 argument), but the argument word is written
// twice. EVM last-write-wins, so the revert argument is 0xbbbb. A reverse scan that keeps the
// first (earliest) match would collapse this to an error carrying 0xaaaa.
mstore(0, 0x1234567800000000000000000000000000000000000000000000000000000000)
mstore(4, 0xaaaa)
mstore(4, 0xbbbb)
revert(0, 0x24)
}
}
}
@@ -26,4 +26,20 @@ contract DivisionArithmetics {
r := smod(n, d)
}
}
/// Regression / soundness PoC: newyork simplifier folds `div(x, x) → 1`
/// and `sdiv(x, x) → 1` for any runtime value `x`, but EVM defines
/// division by zero to return 0, so `div(0, 0) = 0`. The fold is
/// unsound when `x == 0`.
///
/// `mod(x, x) → 0` is sound because EVM also returns 0 for mod by zero.
function divSelf(uint x) external pure returns (uint r) {
assembly { r := div(x, x) }
}
function sdivSelf(int x) external pure returns (int r) {
assembly { r := sdiv(x, x) }
}
function modSelf(uint x) external pure returns (uint r) {
assembly { r := mod(x, x) }
}
}
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Probe: abi.encodePacked tight-packing of mixed-width values + keccak256
/// (signature/CREATE2/mapping-key hashing). Must match EVM exactly.
contract EncodePackedHash {
function h(uint8 a, uint256 b, address c, uint16 d, bytes memory e) external pure returns (bytes32) {
return keccak256(abi.encodePacked(a, b, c, d, e));
}
function h2(uint256 a, uint256 b) external pure returns (bytes32) {
return keccak256(abi.encodePacked(a, b));
}
function h3(string memory s, uint8 n) external pure returns (bytes32) {
return keccak256(abi.encodePacked(s, n));
}
}
@@ -0,0 +1,26 @@
object "EscapeProbe" {
code {
datacopy(0, dataoffset("EscapeProbe_deployed"), datasize("EscapeProbe_deployed"))
return(0, datasize("EscapeProbe_deployed"))
}
object "EscapeProbe_deployed" {
code {
let op := calldataload(0)
let a := calldataload(32)
switch op
case 0 { mstore(0, a) log0(0, 32) return(0, 0) }
case 1 { mstore(0, a) log1(0, 32, 0x42) return(0, 0) }
case 2 { mstore(0x40, a) log0(0x40, 32) return(0, 0) }
case 3 { let p := mload(0x40) mstore(p, a) log0(p, 64) return(0, 0) }
case 4 { mstore(0, a) mstore(32, not(a)) log0(0, 64) return(0, 0) }
case 5 { mstore(0, a) revert(0, 32) }
case 6 { mstore(0x40, a) mstore8(0x60, 0xCD) log0(0x40, 33) return(0, 0) }
case 7 { mstore(0, a) return(0, 32) }
case 8 { mstore(0x40, a) return(0x40, 32) }
case 9 { mstore(0, a) mstore8(7, 0x99) revert(0, 32) }
case 10 { mstore(0, a) log2(0, 32, a, 0x11) return(0, 0) }
case 11 { let p := mload(0x40) mstore(p, a) mstore(add(p, 32), a) return(p, 64) }
default { return(0, 0) }
}
}
}
+9
View File
@@ -9,6 +9,15 @@ contract ExtCode {
}
}
// Two distinct addresses queried in a single function body: regression guard
// for the `code_size` import memory-attribute bug where LLVM CSE'd the two
// syscalls into one and returned `2 * extcodesize(a)`.
function ExtCodeSizeSum(address a, address b) public view returns (uint ret) {
assembly {
ret := add(extcodesize(a), extcodesize(b))
}
}
function CodeSize() public pure returns (uint ret) {
assembly {
ret := codesize()
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// Regression test: factorial with uint8 parameter and uint64 result.
// Bug: newyork pipeline doesn't revert for out-of-range uint8 parameter,
// causing the function to return 1 (factorial of truncated 0) instead of
// reverting.
/* runner.json
{
"differential": true,
"actions": [
{
"Instantiate": {
"code": { "Solidity": { "contract": "Factorial", "solc_optimizer": false } }
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "ebbee3910000000000000000000000000000000000000000000000000000000000000005"
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "ebbee3910000000000000000000000000000000000000000000000000000000100000000"
}
}
]
}
*/
contract Factorial {
function main(uint8 n) public pure returns (uint64) {
uint64 fact = 1;
for (uint8 i = 1; i <= n; i++) {
fact *= i;
}
return fact;
}
}
+19
View File
@@ -0,0 +1,19 @@
object "FmpBig" {
code { datacopy(0, dataoffset("FmpBig_deployed"), datasize("FmpBig_deployed")) return(0, datasize("FmpBig_deployed")) }
object "FmpBig_deployed" {
code {
let op := calldataload(0)
let v := calldataload(32)
let r := 0
switch op
// direct untrusted store to FMP then load
case 0 { mstore(0x40, v) r := mload(0x40) }
// allocator-looking store (add(mload(0x40), v)) then load
case 1 { mstore(0x40, add(mload(0x40), v)) r := mload(0x40) }
// store big, use as offset
case 2 { mstore(0x40, v) let p := mload(0x40) r := p }
mstore(0, r)
return(0, 32)
}
}
}
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract FmpCrossObjectBug {
// Recursive so solc can't inline; the function survives to
// newyork IR as `fun_rec` with parameter ValueIds starting at 0,
// colliding with the deploy (parent) object's `v0 := 0x80`.
function rec(uint256 value, uint256 depth) internal view returns (uint256 r) {
if (depth == 0) {
assembly {
mstore(0x40, value)
// Force the same byte-swap-mode path as FmpRangeProofBug
// so the range proof is the only optimization controlling
// the loaded value.
let _success := staticcall(gas(), origin(), 0, 0x80, 0, 0)
r := mload(0x40)
}
return r;
}
return rec(value, depth - 1);
}
fallback() external {
uint256 value;
uint256 depth;
assembly {
value := calldataload(0)
depth := calldataload(0x20)
}
uint256 result = rec(value, depth);
assembly {
mstore(0x100, result)
return(0x100, 32)
}
}
}
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: same shape as `FmpRevertBug` but with *dynamic*
/// offset and length on the revert. The original `mark_escaping_range`
/// only sets `has_dynamic_escapes` for fully-dynamic escapes
/// (`(None, _)` arm) — it does not lower `min_dynamic_escape_start`,
/// so `fmp_native_safe()` doesn't reject and `mstore(0x40, …)` still
/// encodes as a 4-byte i32 LE native store. With the caller passing
/// `offset = 0` and `length = 96`, the revert covers the FMP slot
/// and the BE/LE encoding mismatch is observed.
contract FmpDynRevertBug {
fallback() external {
uint256 offset;
uint256 length;
assembly {
mstore(0x40, 0xdeadbeef)
offset := calldataload(0)
length := calldataload(32)
revert(offset, length)
}
}
}
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: `mem_opt::FmpPropagation::propagate_statements`
/// tracks the known FMP value across statements. Its MStore handler
/// detects FMP writes by `region == FreePointerSlot ||
/// resolved_offset == Some(0x40)`. For an mstore with a *dynamic*
/// offset whose runtime value happens to be 0x40 (but the simplifier
/// can't see that — e.g. `calldataload(...)`), neither condition
/// fires: the simplifier leaves `region == Unknown` because the
/// offset isn't a literal, and `resolve_offset` returns None because
/// the value isn't tracked in the local constants map. The
/// `fmp_value` stays stale.
///
/// A subsequent `mload(0x40)` IS recognized as an FMP load (because
/// 0x40 is a literal). FmpPropagation folds it to the stale literal
/// `fmp_value` at IR level — diverging from EVM which reads the
/// overwritten memory.
///
/// Both values (`0x100`, `0x200`) fit in the heap-size range so the
/// FMP-load range proof (`to_llvm.rs::apply_range_proof`) doesn't
/// affect this test.
contract FmpDynStoreBug {
fallback() external {
uint256 offset;
assembly {
// Set FMP via a recognized FMP-slot mstore.
mstore(0x40, 0x100)
// Caller passes 0x40 as calldata so offset = 0x40 at
// runtime, but the simplifier sees an opaque calldataload.
offset := calldataload(0)
// Dynamic-offset mstore. FmpPropagation can't tell this
// hits 0x40, so it doesn't invalidate the known fmp_value.
mstore(offset, 0x200)
let v := mload(0x40)
mstore(0x100, v)
return(0x100, 32)
}
}
}
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract FmpNativeStoreBug {
// Recursive — solc can't inline.
function loop(uint depth) internal pure returns (uint) {
if (depth == 0) return 0;
return loop(depth - 1) + depth;
}
fallback() external {
uint256 v;
uint256 depth;
assembly {
v := calldataload(0)
depth := calldataload(0x20)
mstore(0x40, v)
}
// Recursive call: mem_opt clears memory_state, no escape so
// fmp_word_escapes stays false, fmp_native_safe stays true.
uint256 unused = loop(depth);
uint256 r;
assembly {
r := mload(0x40)
// use unused so solc doesn't DCE the call
r := add(r, mul(unused, 0))
mstore(0x100, r)
return(0x100, 32)
}
}
}
@@ -0,0 +1,15 @@
/// Track an FMP value, then overwrite the FMP slot via a calldatacopy whose
/// source is beyond calldatasize (zero-fill). A stale FMP-propagation would
/// forward the tracked value; EVM reads the (now zeroed) slot.
object "FmpPropCopy" {
code { datacopy(0, dataoffset("FmpPropCopy_deployed"), datasize("FmpPropCopy_deployed")) return(0, datasize("FmpPropCopy_deployed")) }
object "FmpPropCopy_deployed" {
code {
mstore(0x40, 0x1234)
calldatacopy(0x40, 1000, 32) // src 1000 >= calldatasize -> zero-fills heap[0x40..0x60]
let p := mload(0x40) // EVM: 0; stale FmpProp would give 0x1234
mstore(0x80, p)
return(0x80, 32)
}
}
}
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract FmpRangeProofBug {
fallback() external {
assembly {
let v := calldataload(0)
mstore(0x40, v)
// Force fmp_word_escapes via a staticcall whose args cover
// memory[0..0x80] (so 0x40 is in the escape range). That
// disables FMP-native-mode storage and forces ByteSwap mode
// where the range-proof bug lives.
let _success := staticcall(gas(), origin(), 0, 0x80, 0, 0)
let r := mload(0x40)
mstore(0x100, r)
return(0x100, 32)
}
}
}
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: `heap_opt.rs::fmp_native_safe()` does NOT check
/// `tainted_regions` or `escaping_regions`. A `revert(0, 96)` that
/// covers the FMP slot (0x40) only calls `mark_escaping_range`
/// (line 241-243), which adds the FMP word to `escaping_regions` /
/// `tainted_regions` — but `fmp_native_safe()` only looks at four flags
/// (variable_accessed_offsets, has_dynamic_accesses,
/// has_return_covering_fmp, min_dynamic_escape_start) and returns
/// true when the static `revert(0, 96)` set none of them.
///
/// `Statement::Return` correctly sets `has_return_covering_fmp` in this
/// case (line 252-254). `Statement::Revert` does not, so a contract
/// that reverts with data covering the FMP gets the FMP slot stored
/// as native i32 little-endian (4 bytes at heap[0x40..0x44]) instead
/// of the big-endian 32-byte word EVM produces.
///
/// EVM revert data [0x40..0x60) = 32-byte BE encoding of FMP value.
/// PVM revert data [0x40..0x60) = 4 bytes LE i32, then 28 bytes
/// of uninitialized/zero heap memory.
contract FmpRevertBug {
fallback() external {
assembly {
// Set FMP to 0xdeadbeef. With newyork's InlineNative mode,
// this becomes a 4-byte i32 LE store at heap[0x40..0x44].
mstore(0x40, 0xdeadbeef)
// Revert with 96 bytes covering [0x00..0x60). EVM caller
// sees 32-byte BE encoding of FMP at bytes [0x40..0x60).
// PVM caller sees the 4-byte LE encoding at [0x40..0x44)
// followed by 28 uninitialized bytes — different bytes.
revert(0, 96)
}
}
}
@@ -0,0 +1,18 @@
/// Soundness PoC candidate: store a meaningful value into the free-memory-
/// pointer slot (0x40) and return a region covering it. If heap native-mode
/// stores 0x40 little-endian while the `return` reads it big-endian, the
/// returned bytes are byte-swapped relative to EVM.
object "FmpSlotReturnByteOrder" {
code {
let size := datasize("FmpSlotReturnByteOrder_deployed")
codecopy(0, dataoffset("FmpSlotReturnByteOrder_deployed"), size)
return(0, size)
}
object "FmpSlotReturnByteOrder_deployed" {
code {
let v := calldataload(0)
mstore(0x40, v)
return(0x40, 32)
}
}
}
@@ -0,0 +1,33 @@
/// Soundness PoC: newyork forward inference gives a `FreePointerSlot` `mload`
/// (the free-memory pointer at 0x40) the width I32, sound only while the FMP
/// holds a Solidity-allocator pointer (`< heap_size`). When a write to 0x40 is
/// not provably sbrk-bounded, `heap_opt` sets `fmp_could_be_unbounded` and
/// codegen loads the *full* FMP word (no `FMP < heap_size` range proof) — but
/// inference still said I32, so `gt(v, 0xffffffff)` was evaluated at 32 bits and
/// truncated the live value.
///
/// `mstore(0x40, calldataload(0))` taints the FMP with an arbitrary value; the
/// `staticcall` is a memory barrier so the following `mload(0x40)` is a real load
/// rather than store-forwarded. With `v = 2^40` (> 2^32) the comparison must be
/// `1`; the bug truncates `v` to its low 32 bits (`0`) and yields `0`.
object "FmpUnboundedCompare" {
code {
let size := datasize("FmpUnboundedCompare_deployed")
codecopy(0, dataoffset("FmpUnboundedCompare_deployed"), size)
return(0, size)
}
object "FmpUnboundedCompare_deployed" {
code {
let taint := calldataload(0)
mstore(0x40, taint)
let ok := staticcall(gas(), 0, 0, 0, 0, 0)
let v := mload(0x40)
let out := 0
if gt(v, 0xffffffff) {
out := 1
}
mstore(0, out)
return(0, 32)
}
}
}
+23
View File
@@ -0,0 +1,23 @@
object "GF" {
code { datacopy(0, dataoffset("GF_deployed"), datasize("GF_deployed")) return(0, datasize("GF_deployed")) }
object "GF_deployed" {
code {
function f(p0, p1) -> r0, r1 {
r0 := 0 r1 := 0
if xor(4294967295, 32) { r0 := p1 leave }
if shr(p1, p1) { r0 := p1 leave }
if 32 { r1 := and(sub(340282366920938463463374607431768211456, 1461501637330902918203684832716283019655932542975), 0xFF) leave }
if sar(255, p1) { r1 := and(smod(5315799647723455265, p1), 0xFF) leave }
}
let a := calldataload(0)
let b := calldataload(32)
let c := calldataload(64)
let d := calldataload(96)
let s0, s1 := f(a, b)
let t0, t1 := f(c, d)
let r := xor(xor(s0, s1), xor(t0, t1))
mstore(0, r)
return(0, 32)
}
}
}
@@ -0,0 +1,33 @@
/// Soundness/robustness PoC: newyork's SSA builder (`ssa.rs`) does not place a
/// variable declared in a `for`-loop INIT block into the scope visible to the
/// loop BODY and POST blocks. In Yul, the init-block declarations are scoped to
/// the entire `for` statement (condition, body, post). The canonical Yul loop
///
/// for { let j := 0 } lt(j, a) { j := add(j, 1) } { ... use j ... }
///
/// therefore panics with `ICE: SsaBuilder::assign called for undeclared
/// variable 'j'` when the body/post references `j`.
///
/// solc's ForLoopInitRewriter hoists init declarations out of the init block
/// (emitting an empty init), so the Solidity frontend — even inline assembly —
/// never produces this shape. Direct Yul input (`resolc --yul` / `Code::Yul`)
/// does, and fails to compile. Fail-safe (ICE, not a silent miscompile), but a
/// valid program the newyork pipeline cannot compile.
object "ForInitScopeIce" {
code {
let size := datasize("ForInitScopeIce_deployed")
codecopy(0, dataoffset("ForInitScopeIce_deployed"), size)
return(0, size)
}
object "ForInitScopeIce_deployed" {
code {
let a := calldataload(0)
let sum := 0
for { let j := 0 } lt(j, a) { j := add(j, 1) } {
sum := add(sum, j)
}
mstore(0x80, sum)
return(0x80, 32)
}
}
}
@@ -0,0 +1,26 @@
object "ForLoopFmp" {
code { let s := datasize("ForLoopFmp_deployed") codecopy(0, dataoffset("ForLoopFmp_deployed"), s) return(0, s) }
object "ForLoopFmp_deployed" {
code {
// Establish a known free-memory pointer so FMP propagation tracks it as a constant.
mstore(0x40, 0x80)
// Dynamic trip count from calldata keeps the loop from being unrolled (which would hide the
// per-iteration re-read of the FMP).
let n := calldataload(0)
// Each iteration allocates a fresh 32-byte slot at the current FMP and bumps the pointer.
// The loop-top `mload(0x40)` must observe the bump from the previous iteration; if FMP
// propagation rewrites it to the pre-loop constant, every iteration aliases the first slot.
for { let i := 0 } lt(i, n) { i := add(i, 1) } {
let p := mload(0x40)
mstore(p, add(i, 1))
mstore(0x40, add(p, 0x20))
}
// Read back the first three allocated slots. Correct: [1, 2, 3] for n >= 3.
// With the aliasing bug: [n, 0, 0].
mstore(0x200, mload(0x80))
mstore(0x220, mload(0xa0))
mstore(0x240, mload(0xc0))
return(0x200, 0x60)
}
}
}
@@ -0,0 +1,66 @@
/// Soundness PoC: newyork's fuzzy function deduplication
/// (`simplify.rs::deduplicate_functions_fuzzy`) abstracts `switch` case
/// match values into parameterizable literal positions
/// (`fuzzy_encode_statement` emits a placeholder per case value), so two
/// functions that are structurally identical *except for their switch case
/// match values* hash to the same fuzzy form and are merged.
///
/// However `replace_literals_with_params` advances the literal counter for
/// each switch case but NEVER substitutes `case.value`. The merged canonical
/// function therefore keeps ITS OWN case values; the removed function's call
/// sites are redirected to the canonical function with the (now ignored)
/// differing values appended as dead extra arguments.
///
/// Result: `g`'s callers silently execute `f`'s switch dispatch. `g(111)`
/// should hit `g`'s `case 111` and return 1111, but after the merge it falls
/// through to the default of `f` (whose cases are 100/200/300) and returns
/// the default value. EVM (and unoptimized semantics) returns 1111.
object "FuzzySwitchBug" {
code {
let size := datasize("FuzzySwitchBug_deployed")
codecopy(0, dataoffset("FuzzySwitchBug_deployed"), size)
return(0, size)
}
object "FuzzySwitchBug_deployed" {
code {
let sel := calldataload(0)
let x := calldataload(32)
let r := 0
switch sel
case 0 { r := f(x) }
case 1 { r := f(x) }
case 2 { r := g(x) }
case 3 { r := g(x) }
default { r := 0 }
mstore(0, r)
return(0, 32)
function f(a) -> out {
let t := add(a, 1)
t := mul(t, 2)
t := add(t, 3)
t := xor(t, 4)
t := add(t, 5)
out := 0
switch a
case 100 { out := add(t, 1000) }
case 200 { out := add(t, 2000) }
case 300 { out := add(t, 3000) }
default { out := 9999 }
}
function g(a) -> out {
let t := add(a, 1)
t := mul(t, 2)
t := add(t, 3)
t := xor(t, 4)
t := add(t, 5)
out := 0
switch a
case 111 { out := add(t, 1000) }
case 222 { out := add(t, 2000) }
case 333 { out := add(t, 3000) }
default { out := 9999 }
}
}
}
}
@@ -0,0 +1,17 @@
object "HugeConstOffsetStore" {
code { let s := datasize("HugeConstOffsetStore_deployed") codecopy(0, dataoffset("HugeConstOffsetStore_deployed"), s) return(0, s) }
object "HugeConstOffsetStore_deployed" {
code {
let x := calldataload(0)
// A dynamic store keeps the object out of all-native mode, so the huge CONSTANT-offset store
// below takes the inline (unchecked) path rather than the bounds-checked AllNative path.
mstore(calldataload(32), 7)
// Huge constant offset: far past the fixed heap. EVM charges astronomical memory-expansion gas
// and runs out of gas; the inline unchecked GEP would instead write out of the heap global. It
// must trap (out-of-gas), not corrupt memory and fall through to the sstore.
mstore(0xFFFFFFF0, x)
sstore(0, 1)
return(0, 0)
}
}
}
@@ -0,0 +1,26 @@
object "IfCallFmp" {
code { let s := datasize("IfCallFmp_deployed") codecopy(0, dataoffset("IfCallFmp_deployed"), s) return(0, s) }
object "IfCallFmp_deployed" {
code {
// Known free-memory pointer, tracked as a constant by FMP propagation.
mstore(0x40, 0x80)
let cnt := calldataload(0)
// The branch calls an internal allocator that bumps the FMP. `allocate` is recursive, so it is
// never inlined: the `mstore(0x40, ...)` lives inside the callee, not inside this `if` body.
// Region FMP invalidation must therefore consult `fmp_writers` (the call), not just look for a
// direct FMP store in the branch — otherwise the post-`if` `mload(0x40)` is forwarded the stale
// pre-branch constant 0x80 even when `cnt > 0` bumped the pointer.
if cnt { allocate(cnt) }
let p := mload(0x40)
mstore(0x200, p)
return(0x200, 0x20)
function allocate(n) {
if n {
let f := mload(0x40)
mstore(0x40, add(f, 0x20))
allocate(sub(n, 1))
}
}
}
}
}
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// Regression test: contracts whose Yul IR generates internal helper functions
// (e.g. abi_encode_string, extract_byte_array_length) that survive the newyork
// inliner require a function scope in the LLVM context during code generation.
// Without it, declaring frontend functions in generate_object triggers an ICE:
// "function scope must be pushed before declaring frontend functions".
/* runner.json
{
"differential": false,
"actions": [
{
"Instantiate": {}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "e942b51600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000568656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005776f726c64000000000000000000000000000000000000000000000000000000"
}
},
{
"VerifyCall": {
"success": true
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "6d4ce63c"
}
},
{
"VerifyCall": {
"success": true,
"output": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000568656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005776f726c64000000000000000000000000000000000000000000000000000000"
}
}
]
}
*/
contract InternalFn {
string public a;
string public b;
function set(string calldata _a, string calldata _b) external {
a = _a;
b = _b;
}
function get() external view returns (string memory, string memory) {
return (a, b);
}
}
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract KeccakFuseBug {
address public target;
constructor() {
target = address(this);
}
function probe(uint256[8] calldata seeds)
external view returns (uint256 r, bytes32 sink_out)
{
bytes32 sink;
uint256 s0 = seeds[0]; uint256 s1 = seeds[1]; uint256 s2 = seeds[2];
uint256 s3 = seeds[3]; uint256 s4 = seeds[4]; uint256 s5 = seeds[5];
uint256 s6 = seeds[6]; uint256 s7 = seeds[7];
assembly {
mstore(0, s0) sink := xor(sink, keccak256(0, 32))
mstore(0, s1) sink := xor(sink, keccak256(0, 32))
mstore(0, s2) sink := xor(sink, keccak256(0, 32))
mstore(0, s3) sink := xor(sink, keccak256(0, 32))
mstore(0, s4) sink := xor(sink, keccak256(0, 32))
mstore(0, s5) sink := xor(sink, keccak256(0, 32))
mstore(0, s6) sink := xor(sink, keccak256(0, 32))
mstore(0, s7) sink := xor(sink, keccak256(0, 32))
}
(bool ok,) = target.staticcall("");
ok;
assembly {
r := mload(0)
}
sink_out = sink;
}
}
@@ -0,0 +1,44 @@
/// Soundness PoC: newyork forward type inference widens an `if`/`switch`
/// output only from the then/else region *yields*, never from `inputs`. But on
/// a missing `else`/`default` edge codegen routes `inputs` straight through to
/// `outputs` (`to_llvm` phi construction; the IR `If` "defaults to yielding
/// inputs unchanged" contract). The `leave`-elimination wrapper
/// (`inline.rs::wrap_remaining_in_guard`) builds exactly that shape:
/// `else_region: None`, non-empty `outputs`, where `inputs` are the
/// pre-`leave` accumulators and the then-region yields the fall-through values.
///
/// `f(v)` sets `ret := v` (full width), takes `leave` for `v > 1000` (carrying
/// the wide `ret`), and otherwise falls through to `ret := 7` (narrow). Inference
/// widened the output only from the `7` yield → i8, so codegen truncates the
/// leave-edge value to its low byte. `from_yul` always emits an explicit else,
/// so only the post-inlining `leave` shape is affected.
///
/// Called with `v = 2^200` (> 1000): EVM returns `2^200`; the bug returns
/// `2^200 mod 256 == 0`.
object "LeaveWideOutput" {
code {
let size := datasize("LeaveWideOutput_deployed")
codecopy(0, dataoffset("LeaveWideOutput_deployed"), size)
return(0, size)
}
object "LeaveWideOutput_deployed" {
code {
let x := calldataload(0)
let r := f(x)
let out := 0
if gt(r, 5) {
out := 1
}
mstore(0, out)
return(0, 32)
function f(v) -> ret {
ret := v
if gt(v, 1000) {
leave
}
ret := 7
}
}
}
}
@@ -0,0 +1,14 @@
/// Store a value at 0x80, then overwrite [0x80,0xA0) with calldata via calldatacopy.
/// A stale load-forward would return the original stored value.
object "LoadFwdCopy" {
code { datacopy(0, dataoffset("LoadFwdCopy_deployed"), datasize("LoadFwdCopy_deployed")) return(0, datasize("LoadFwdCopy_deployed")) }
object "LoadFwdCopy_deployed" {
code {
mstore(0x80, 0x1234)
calldatacopy(0x80, 0, 32)
let p := mload(0x80) // EVM: calldata[0..32]; stale fwd: 0x1234
mstore(0x100, p)
return(0x100, 32)
}
}
}
+11 -1
View File
@@ -4,7 +4,7 @@ pragma solidity ^0.8;
/* runner.json
{
"differential": true,
"differential": false,
"actions": [
{
"Instantiate": {
@@ -24,6 +24,11 @@ pragma solidity ^0.8;
"data": "f016832c"
}
},
{
"VerifyCall": {
"success": true
}
},
{
"Call": {
"dest": {
@@ -31,6 +36,11 @@ pragma solidity ^0.8;
},
"data": "f4a63aa5"
}
},
{
"VerifyCall": {
"success": true
}
}
]
}
+15
View File
@@ -0,0 +1,15 @@
object "Mem27" {
code { datacopy(0, dataoffset("Mem27_deployed"), datasize("Mem27_deployed")) return(0, datasize("Mem27_deployed")) }
object "Mem27_deployed" {
code {
let r := 0
r := xor(r, mload(16))
r := xor(r, keccak256(16, 29))
r := xor(r, mload(16))
r := add(r, sload(13))
r := xor(r, keccak256(0, 15))
mstore(0, r)
return(0, 32)
}
}
}
@@ -0,0 +1,36 @@
/// Regression fixture for the dead-store-elimination key-mismatch bug in the
/// newyork mem_opt pass. `MemoryOptimizer.pending_stores` is keyed by the exact
/// store offset, but the load handler removed entries by the word-aligned offset.
///
/// For a store at the non-word-aligned offset `0x104`, the exact `mload(0x104)`
/// forwards the stored value but (with the bug) removes the word-aligned key
/// `0x100` instead of `0x104`, so the store is never marked read. The later
/// same-offset `mstore(0x104, v2)` then finds the still-pending entry and
/// dead-eliminates the first store — even though `mload(0x108)` (overlapping,
/// non-forwarded) read its bytes. The eliminated store makes `y` observe fresh
/// memory instead of `v1`, diverging from the EVM reference.
///
/// The `keccak256(0x104, 0x40)` forces the region to big-endian (escaping)
/// memory, so the only behavioural divergence under the bug is the dropped
/// store, not memory byte order. Compiled directly through revive's Yul path so
/// the exact statement shape reaches the newyork mem_opt pass intact.
object "MemOptOverlapDeadStore" {
code {
let size := datasize("MemOptOverlapDeadStore_deployed")
codecopy(0, dataoffset("MemOptOverlapDeadStore_deployed"), size)
return(0, size)
}
object "MemOptOverlapDeadStore_deployed" {
code {
let v1 := calldataload(0)
let v2 := calldataload(32)
mstore(0x104, v1)
let x := mload(0x104)
let y := mload(0x108)
mstore(0x104, v2)
let h := keccak256(0x104, 0x40)
mstore(0, xor(xor(x, y), h))
return(0, 32)
}
}
}
+34
View File
@@ -0,0 +1,34 @@
object "MemProbe" {
code {
datacopy(0, dataoffset("MemProbe_deployed"), datasize("MemProbe_deployed"))
return(0, datasize("MemProbe_deployed"))
}
object "MemProbe_deployed" {
code {
let op := calldataload(0)
let a := calldataload(32)
let r := 0
switch op
case 0 { mstore(0, a) r := keccak256(0, 32) }
case 1 { mstore(0, a) mstore(32, a) r := keccak256(0, 64) }
case 2 { mstore(0, a) r := mload(0) }
case 3 { mstore(0, a) mstore(16, a) r := mload(0) }
case 4 { mstore8(0, a) r := mload(0) }
case 5 { mstore(0, a) r := keccak256(0, 1) }
case 6 { mstore(0, a) calldatacopy(32, calldatasize(), 32) r := keccak256(0, 64) }
case 7 { mstore(0, a) mcopy(32, 0, 32) r := keccak256(32, 32) }
case 8 { mstore(0, a) r := msize() }
case 9 { mstore(0x80, a) r := msize() }
case 10 { mstore8(100, 0xAB) r := msize() }
case 11 { mstore(0, a) mstore(32, a) mcopy(8, 0, 40) r := keccak256(0, 72) }
case 12 { mstore(0, a) r := keccak256(0, 0) }
case 13 { let p := mload(0x40) mstore(p, a) r := keccak256(p, 32) }
case 14 { mstore(0, a) r := keccak256(3, 17) }
case 15 { mstore(0, a) mstore8(5, 0xEE) r := mload(0) }
case 16 { mstore(0, a) r := keccak256(31, 1) }
case 17 { mstore(10, a) r := mload(0) }
mstore(0, r)
return(0, 32)
}
}
}
@@ -0,0 +1,16 @@
object "MsizeCopy" {
code { let s := datasize("MsizeCopy_deployed") codecopy(0, dataoffset("MsizeCopy_deployed"), s) return(0, s) }
object "MsizeCopy_deployed" {
code {
let op := calldataload(0)
let r := 0
switch op
case 0 { calldatacopy(0x81, 0, 5) r := msize() } // dest 0x81 len 5 -> end 0x86 -> EVM 0xA0
case 1 { mstore(0x80, 1) mcopy(0x141, 0x80, 7) r := msize() } // dest 0x141 len 7 -> 0x148 -> 0x160
case 2 { calldatacopy(0x100, 0, 33) r := msize() } // 0x100+33=0x121 -> 0x140
default { r := 0 }
mstore(0x400, r)
return(0x400, 32)
}
}
}
@@ -0,0 +1,23 @@
object "MsizeForCondition" {
code { let s := datasize("MsizeForCondition_deployed") codecopy(0, dataoffset("MsizeForCondition_deployed"), s) return(0, s) }
object "MsizeForCondition_deployed" {
code {
let x := calldataload(0)
// Touch memory at 0x80 so the EVM free-memory watermark (msize) is a
// nonzero 0xA0, regardless of `x`.
mstore(0x80, x)
let ran := 0
// A *bare* `msize()` for-loop condition: this is the only expression
// position the IR does not materialize into a preceding `let`. The body
// runs once iff `msize()` is truthy. If a native store fails to update
// the heap-size watermark (because `has_msize` missed this position),
// `msize()` reads stale 0, the body is skipped, and `ran` diverges.
for {} msize() {} {
ran := add(ran, 1)
break
}
mstore(0x400, ran)
return(0x400, 32)
}
}
}
@@ -0,0 +1,25 @@
/// Probe: msize after various memory-touching ops. EVM msize = highest accessed
/// byte rounded up to a word. newyork tracks msize via ensure_heap_size; any
/// divergence corrupts Solidity's allocator (which reads msize/FMP).
object "MsizeProbe" {
code { let s := datasize("MsizeProbe_deployed") codecopy(0, dataoffset("MsizeProbe_deployed"), s) return(0, s) }
object "MsizeProbe_deployed" {
code {
let op := calldataload(0)
let x := calldataload(32)
let r := 0
switch op
case 0 { mstore(0x80, x) r := msize() }
case 1 { mstore8(0x95, x) r := msize() }
case 2 { let _v := mload(0x100) r := msize() }
case 3 { mstore(0x80, x) calldatacopy(0x200, 0, 16) r := msize() }
case 4 { r := msize() }
case 5 { mstore(0x80, x) let _v := mload(0x81) r := msize() }
case 6 { mstore8(0x100, x) r := msize() }
case 7 { mstore(and(x, 0xfe0), x) r := msize() }
default { r := 0 }
mstore(0x400, r)
return(0x400, 32)
}
}
}
@@ -0,0 +1,16 @@
object "MsizeReadOps" {
code { let s := datasize("MsizeReadOps_deployed") codecopy(0, dataoffset("MsizeReadOps_deployed"), s) return(0, s) }
object "MsizeReadOps_deployed" {
code {
let op := calldataload(0)
let r := 0
switch op
case 0 { mstore(0,0) let h := keccak256(0x81, 5) r := msize() } // reads 0x81..0x86 -> EVM 0xA0
case 1 { log0(0x81, 7) r := msize() } // 0x81..0x88 -> EVM 0xA0
case 2 { let h := keccak256(0xc1, 33) r := msize() } // 0xc1..0xe2 -> EVM 0x100
default { r := 0 }
mstore(0x400, r)
return(0x400, 32)
}
}
}
@@ -0,0 +1,18 @@
object "MsizeUnaligned" {
code { let s := datasize("MsizeUnaligned_deployed") codecopy(0, dataoffset("MsizeUnaligned_deployed"), s) return(0, s) }
object "MsizeUnaligned_deployed" {
code {
let op := calldataload(0)
let x := calldataload(32)
let r := 0
switch op
case 0 { mstore(0x81, x) r := msize() } // EVM: ceil(0xA1)->0xC0 (192)
case 1 { mstore8(0x85, x) r := msize() } // EVM: ceil(0x86)->0xA0 (160)
case 2 { mstore(0x80, x) r := msize() } // aligned: 0xA0 (160)
case 3 { mstore(0xc1, x) r := msize() } // EVM: ceil(0xE1)->0x100 (256)
default { r := 0 }
mstore(0x400, r)
return(0x400, 32)
}
}
}
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: newyork's `narrow_function_returns` only inspects the
/// function's fall-through `return_values`, ignoring values returned by EARLY
/// `leave` (return) statements. When the fall-through returns a small constant
/// (here `return 0`, min_width I1) but earlier branches return full-width
/// values, the function's return type is narrowed to i32 and EVERY early-leave
/// result is truncated to 32 bits. `op(a,b)` for a = 2^256-1 returns
/// 0xffffffff instead of 2^256-1.
contract MultiLeaveReturnNarrow {
function run(uint256 op, uint256 a, uint256 b) external pure returns (uint256) {
if (op == 0) return a + b;
if (op == 1) return a - b;
if (op == 2) return a * b;
if (op == 3) return a / b;
if (op == 4) return a % b;
return 0; // small-constant fall-through
}
}
@@ -0,0 +1,44 @@
object "NarrowProbe" {
code {
datacopy(0, dataoffset("NarrowProbe_deployed"), datasize("NarrowProbe_deployed"))
return(0, datasize("NarrowProbe_deployed"))
}
object "NarrowProbe_deployed" {
code {
let op := calldataload(0)
let a := calldataload(32)
let r := 0
switch op
// high-bit round trip: narrow via shr then widen back via shl
case 0 { r := shl(200, shr(200, a)) }
// narrow to i32 then square (needs up to 64 bits)
case 1 { let t := and(a, 0xFFFFFFFF) r := mul(t, t) }
// narrow to i64 then shift into the high half
case 2 { let t := shr(192, a) r := shl(192, t) }
// narrow store then load round trip (i16)
case 3 { let t := and(a, 0xFFFF) mstore(0, t) r := mload(0) }
// top byte (i8) shifted back to the top
case 4 { let t := byte(0, a) r := shl(248, t) }
// narrow to i8 then storage round trip
case 5 { let t := and(a, 0xFF) sstore(5, t) r := sload(5) }
// narrow then negate: 0 - small => full-width large value
case 6 { let t := shr(128, a) r := sub(0, t) }
// not of a narrow value => high bits all set (full width)
case 7 { let t := and(a, 0xFFFFFFFF) r := not(t) }
// i64 + i64 => up to 65 bits
case 8 { let t := and(a, 0xFFFFFFFFFFFFFFFF) r := add(t, t) }
// signextend over a narrowed value
case 9 { let t := shr(64, a) r := signextend(7, t) }
// narrow then compare-against-large: lt(small, big) must be 1
case 10 { let t := and(a, 0xFF) r := lt(t, 0x100000000) }
// div of a narrowed dividend by 1 then widen-add a big constant
case 11 { let t := and(a, 0xFFFF) r := add(div(t, 1), shl(200, 1)) }
// mulmod where two operands are narrow but the product overflows 256
case 12 { let t := and(a, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) r := mulmod(t, t, 0x100000000000000000000000000000000) }
// narrow value used as exp base, large exponent
case 13 { let t := and(a, 0xFF) r := exp(t, 64) }
mstore(0, r)
return(0, 32)
}
}
}
+36
View File
@@ -0,0 +1,36 @@
object "OpProbe" {
code {
datacopy(0, dataoffset("OpProbe_deployed"), datasize("OpProbe_deployed"))
return(0, datasize("OpProbe_deployed"))
}
object "OpProbe_deployed" {
code {
let op := calldataload(0)
let a := calldataload(32)
let b := calldataload(64)
let c := calldataload(96)
let r := 0
switch op
case 0 { r := shl(a, b) }
case 1 { r := shr(a, b) }
case 2 { r := sar(a, b) }
case 3 { r := byte(a, b) }
case 4 { r := signextend(a, b) }
case 5 { r := sdiv(a, b) }
case 6 { r := smod(a, b) }
case 7 { r := exp(a, b) }
case 8 { r := addmod(a, b, c) }
case 9 { r := mulmod(a, b, c) }
case 10 { r := div(a, b) }
case 11 { r := mod(a, b) }
case 12 { r := slt(a, b) }
case 13 { r := sgt(a, b) }
case 14 { r := lt(a, b) }
case 15 { r := gt(a, b) }
case 16 { r := not(a) }
case 17 { r := addmod(mul(a, b), c, a) }
mstore(0, r)
return(0, 32)
}
}
}
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: the panic-pattern outliner in
/// `simplify.rs::find_panic_pattern_backwards` extracts the panic code
/// from `mstore(4, code)` via `code_val.to_u64_digits().first()` and a
/// `<= 0xFF` check. That returns the LEAST-significant u64 digit, so any
/// 256-bit `code` value whose low byte is in `[0, 0xFF]` is mis-classified
/// as a small canonical Solidity panic code regardless of its higher bits.
///
/// Codegen for `Statement::PanicRevert { code }` emits the canonical
/// 36-byte Solidity panic format (selector + zero-padded `code`), so the
/// high bits of the original mstored value are silently dropped from the
/// revert data observed by the caller. EVM emits the original bytes.
contract PanicCodeBug {
fallback() external {
assembly {
// canonical panic selector at offset 0
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
// "code" value 0x10_00000000_00000000_00000000_00000042 — its low
// byte (0x42) passes the simplifier's `<= 0xFF` check, but byte
// position 20 of the revert encoding holds the 0x10 nibble.
mstore(4, 0x10000000000000000000000000000042)
revert(0, 0x24)
}
}
}
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Probe: less-common panic codes' revert data must match EVM.
contract PanicCodes {
enum E { A, B, C }
function enumConv(uint256 x) external pure returns (E) { return E(x); } // 0x21 if x>2
function uninitFp() external pure returns (uint256) {
function(uint256) internal pure returns (uint256) fp;
return fp(5); // 0x51
}
function overAlloc(uint256 n) external pure returns (uint256) {
uint256[] memory a = new uint256[](n); // 0x41 if huge
return a.length;
}
function shiftEdge(uint256 a, uint256 s) external pure returns (uint256) {
return a << s; // no panic, edge
}
}
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: `simplify.rs::find_panic_pattern_backwards` walks
/// backward from `revert(0, 0x24)` skipping every mstore whose offset
/// is not exactly 0 or 4, and the final `only_safe` gate only checks
/// that intermediate statements are `Let`/`MStore`/`Expression`. An
/// `mstore(p, …)` with `p ∈ (0, 4) ∪ (4, 0x24)` partially overwrites
/// the panic-encoded revert data EVM emits, but the simplifier still
/// matches the canonical panic shape and replaces the whole sequence
/// with `Statement::PanicRevert { code }` — dropping the corrupting
/// mstore from the revert data observed by the caller.
contract PanicInterveneBug {
fallback() external {
assembly {
// Canonical panic selector + code prelude.
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
mstore(4, 0x01)
// Unaligned mstore overwriting bytes 7..38 — bytes 7..35 of
// this end up in the revert(0, 0x24) data on EVM. PVM emits
// canonical PanicRevert(0x01) instead, dropping these bytes.
mstore(7, 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef)
revert(0, 0x24)
}
}
}
@@ -0,0 +1,20 @@
object "PanicOutlineCall" {
code { let s := datasize("PanicOutlineCall_deployed") codecopy(0, dataoffset("PanicOutlineCall_deployed"), s) return(0, s) }
object "PanicOutlineCall_deployed" {
code {
// A canonical Panic(uint256) shape, but with an internal call between the selector store and
// the revert. `inner` is recursive so it is never inlined; it reverts with distinct data before
// the panic revert is reached. Collapsing this window into a single PanicRevert would drop the
// call and revert with the wrong (panic) payload.
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
inner(0)
mstore(4, 0x11)
revert(0, 0x24)
function inner(n) {
if n { inner(sub(n, 1)) }
mstore(0x80, 0xdeadbeef)
revert(0x80, 0x20)
}
}
}
}
@@ -0,0 +1,21 @@
object "PanicOutlineYield" {
code { let s := datasize("PanicOutlineYield_deployed") codecopy(0, dataoffset("PanicOutlineYield_deployed"), s) return(0, s) }
object "PanicOutlineYield_deployed" {
code {
let cond := calldataload(0)
let r := 0
// `r` is reassigned inside the branch, between the panic selector store and the revert, and is
// read after the branch — so the branch region yields that in-window definition of `r`.
// Collapsing the window into a PanicRevert drops the definition; without a zero-binding rescue
// the surviving yield references an undefined value and the SSA validator panics.
if cond {
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
r := 7
mstore(4, 0x11)
revert(0, 0x24)
}
sstore(0, r)
return(0, 0)
}
}
}
@@ -0,0 +1,14 @@
object "PanicOverwriteSelector" {
code { let s := datasize("PanicOverwriteSelector_deployed") codecopy(0, dataoffset("PanicOverwriteSelector_deployed"), s) return(0, s) }
object "PanicOverwriteSelector_deployed" {
code {
// Canonical panic selector + code, but the selector word is then overwritten before the
// revert. Last-write-wins means the EVM revert data starts with 0xdeadbeef, not the panic
// selector — so this must NOT be collapsed into a canonical Panic(0x11).
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
mstore(4, 0x11)
mstore(0, 0xdeadbeef)
revert(0, 0x24)
}
}
}
@@ -0,0 +1,59 @@
/// Soundness PoC: newyork parameter narrowing
/// (`type_inference.rs::narrow_function_params`) narrows a parameter to I64/I32
/// whenever its `max_width` is lowered below I256 by a use that re-validates the
/// value (a memory offset bounded by `safe_truncate_int_to_xlen`). The call
/// boundary then emits a *checked* truncation that traps on an out-of-range
/// argument (`to_llvm.rs::checked_truncate_to`).
///
/// The bug: `max_width` was lowered by ANY narrowing use, including one reached
/// only on a conditional path. `store_if(p, c)` uses `p` as a memory offset only
/// inside `if c`. EVM skips the store (and never touches `p`) when `c == 0`, so a
/// `p >= 2^64` argument is harmless. But the narrowed `i64` signature truncates
/// `p` unconditionally at every call site, trapping before `c` is even tested.
///
/// Every caller passes a `calldataload`-derived `p` (forward width I256, so the
/// caller-driven narrowing path cannot narrow it) and four distinct call sites
/// plus the body's arithmetic chain keep `store_if` out of the inliner, so the
/// narrowed parameter reaches codegen.
///
/// Called with `c == 0` and `p == 2^64`: EVM returns `4` (each `store_if`
/// returns its else-path value); the buggy narrowing traps at the first call.
object "ParamCondNarrow" {
code {
let size := datasize("ParamCondNarrow_deployed")
codecopy(0, dataoffset("ParamCondNarrow_deployed"), size)
return(0, size)
}
object "ParamCondNarrow_deployed" {
code {
let c := calldataload(0)
let acc := 0
acc := add(acc, store_if(calldataload(32), c))
acc := add(acc, store_if(calldataload(64), c))
acc := add(acc, store_if(calldataload(96), c))
acc := add(acc, store_if(calldataload(128), c))
mstore(0, acc)
return(0, 32)
function store_if(p, g) -> r {
r := 1
if g {
// `p` is used as a memory offset only on this conditional path.
mstore(p, 0x42)
r := mload(p)
}
// Live arithmetic chain on the condition keeps the body over the
// always-inline size so the narrowed parameter reaches codegen.
let a1 := add(g, 1)
let a2 := mul(a1, 3)
let a3 := xor(a2, 7)
let a4 := add(a3, 9)
let a5 := or(a4, 13)
let a6 := add(a5, 17)
let a7 := mul(a6, 19)
let a8 := xor(a7, 23)
r := add(r, a8)
}
}
}
}
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: function parameter used only as `mload` offset gets
/// silently narrowed from i256 to i64 by `narrow_function_params`. The
/// call-site bare truncate drops the upper 192 bits BEFORE the use-site
/// `safe_truncate_int_to_xlen` can observe them, so `mload(2^64)` aliases
/// to `mload(0)` and returns the zero-initialised scratch slot instead of
/// OOGing on memory expansion the way EVM does.
///
/// `fetch` is intentionally recursive so the early newyork inliner refuses
/// to inline it, preserving the narrowable `mload(x)`-only parameter shape.
contract ParamMload {
function fetch(uint depth, uint x) internal pure returns (uint r) {
if (depth == 0) {
assembly { r := mload(x) }
return r;
}
return fetch(depth - 1, x);
}
function tryFetch(uint x) external pure returns (uint) {
return fetch(1, x);
}
}
@@ -0,0 +1,11 @@
object "PastHeapConstReturn" {
code { let s := datasize("PastHeapConstReturn_deployed") codecopy(0, dataoffset("PastHeapConstReturn_deployed"), s) return(0, s) }
object "PastHeapConstReturn_deployed" {
code {
// Return a constant 32-byte range starting exactly at the heap end (heap is 128 KiB = 0x20000),
// with no prior store. The inline unchecked seal_return would read one-past-heap into the
// returndata (information leak); it must instead trap via the bounds-checked path.
return(0x20000, 0x20)
}
}
}
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract SDivNarrowBug {
/// Forces LLVM IR of the form
/// sdiv i256 (and i256 %a, 0xff..ff_u64), (and i256 %b, 0xff..ff_u64)
///
/// `provable_bit_width` accepts the AND-mask proof and lets revive's
/// `narrow_divrem_instructions` rewrite this to `sdiv i64 (trunc..), (trunc..)`
/// followed by `sext` back to i256. The truncated low-64 view of an i256
/// whose bit 63 is set looks negative under i64, but the i256 value is
/// non-negative (high 192 bits are zero by the AND), so the narrowed
/// signed division returns the wrong sign.
function sdiv_masked(uint256 a, uint256 b) external pure returns (int256 q) {
assembly {
let am := and(a, 0xffffffffffffffff)
let bm := and(b, 0xffffffffffffffff)
q := sdiv(am, bm)
}
}
}
@@ -0,0 +1,55 @@
/// Soundness PoC: newyork forward-width rule for `sar` with a constant shift
/// (`type_inference.rs::infer_expression_width`, the `Shr | Sar` arm):
///
/// if let Some(shift) = known_constants[lhs] {
/// ... else { BitWidth::from_bits((256 - shift).max(1)) }
/// } else { rhs_width }
///
/// For logical `shr` this is correct (the shifted-in high bits are zero, so the
/// result fits in `256 - shift` bits). For ARITHMETIC `sar` it is NOT: when the
/// shifted value is negative, `sar` sign-extends, so the high bits stay set and
/// the result is full-width (~2^256). The rule ignores the operand's sign and
/// caps the width at `256 - shift`. `sar` also never marks the value
/// `is_signed`, so return-type narrowing then truncates the sign bits.
///
/// `doSar` returns `sar(250, value)`. For `value = -1` (2^256-1) the result is
/// `-1` (all ones) on EVM, but the width rule reports `from_bits(6) = I8`
/// (clamped to I32), so the narrowed return drops everything above 32 bits and
/// yields `0xffffffff`.
object "SarReturnNarrow" {
code {
let size := datasize("SarReturnNarrow_deployed")
codecopy(0, dataoffset("SarReturnNarrow_deployed"), size)
return(0, size)
}
object "SarReturnNarrow_deployed" {
code {
let z0, w0 := doSar(calldataload(0), calldataload(32))
let z1, w1 := doSar(calldataload(64), calldataload(96))
let z2, w2 := doSar(calldataload(128), calldataload(160))
let z3, w3 := doSar(calldataload(192), calldataload(224))
mstore(0, z0)
mstore(32, w0)
mstore(64, z1)
mstore(96, w1)
mstore(128, z2)
mstore(160, w2)
mstore(192, z3)
mstore(224, w3)
return(0, 256)
function doSar(value, y) -> z, w {
z := sar(250, value)
let a1 := add(y, 1)
let a2 := mul(a1, 3)
let a3 := xor(a2, 7)
let a4 := add(a3, 9)
let a5 := or(a4, 13)
let a6 := add(a5, 17)
let a7 := mul(a6, 19)
let a8 := xor(a7, 23)
w := add(a8, 29)
}
}
}
}
@@ -0,0 +1,15 @@
/// Register 0x20 (< 0x60, exempt from dynamic-escape guard) as an aligned native candidate.
object "ScratchCopyTaint" {
code { datacopy(0, dataoffset("ScratchCopyTaint_deployed"), datasize("ScratchCopyTaint_deployed")) return(0, datasize("ScratchCopyTaint_deployed")) }
object "ScratchCopyTaint_deployed" {
code {
mstore(0x20, 0)
// Overwrite scratch [0x00,0x40) with big-endian calldata. Taints only word_align(0)=0.
calldatacopy(0, 0, 64)
// Read 0x20: if native-LE, byte-reverses the BE calldata word.
let b := mload(0x20)
mstore(0x80, b)
return(0x80, 32)
}
}
}
@@ -0,0 +1,50 @@
/// Soundness PoC: the under-estimated forward width of `sdiv` (Bug #7) is also
/// consumed by codegen's comparison-operand narrowing
/// (`to_llvm.rs::try_narrow_comparison`), a separate amplifier from return-type
/// narrowing. When a comparison's operand has an (incorrectly) narrow inferred
/// width, both operands are truncated to that width before comparing — so a
/// full-width value masquerading as i32 is silently truncated, giving the wrong
/// comparison result and hence wrong control flow.
///
/// `doCmp` returns `lt(sdiv(x, y), 0xffffffff)`. With `x` narrowed (callers pass
/// `and(calldataload, 0xff)`) and `y = -1`, `sdiv(5, -1) = -5 = 2^256-5`, which
/// is NOT `< 0xffffffff` (EVM result 0). But the comparison truncates the
/// quotient to i32 (`0xfffffffb`) and computes `lt(0xfffffffb, 0xffffffff) = 1`.
object "SdivCompareNarrow" {
code {
let size := datasize("SdivCompareNarrow_deployed")
codecopy(0, dataoffset("SdivCompareNarrow_deployed"), size)
return(0, size)
}
object "SdivCompareNarrow_deployed" {
code {
let r0, w0 := doCmp(and(calldataload(0), 0xff), calldataload(32))
let r1, w1 := doCmp(and(calldataload(64), 0xff), calldataload(96))
let r2, w2 := doCmp(and(calldataload(128), 0xff), calldataload(160))
let r3, w3 := doCmp(and(calldataload(192), 0xff), calldataload(224))
mstore(0x80, r0)
mstore(0xa0, w0)
mstore(0xc0, r1)
mstore(0xe0, w1)
mstore(0x100, r2)
mstore(0x120, w2)
mstore(0x140, r3)
mstore(0x160, w3)
return(0x80, 256)
function doCmp(x, y) -> r, w {
let s := sdiv(x, y)
r := lt(s, 0xffffffff)
let a1 := add(y, 1)
let a2 := mul(a1, 3)
let a3 := xor(a2, 7)
let a4 := add(a3, 9)
let a5 := or(a4, 13)
let a6 := add(a5, 17)
let a7 := mul(a6, 19)
let a8 := xor(a7, 23)
w := add(a8, 29)
}
}
}
}
@@ -0,0 +1,62 @@
/// Soundness PoC: newyork return-type narrowing
/// (`type_inference.rs::narrow_function_returns`) narrows a function's return
/// type to its forward `min_width` whenever `min_width < I256` and the return
/// value is not flagged `is_signed`.
///
/// The forward width rule for `sdiv` is `lhs_width` (the dividend's width)
/// (`infer_expression_width`: `Div | SDiv | Mod | SMod => lhs_width`). For
/// *unsigned* div/mod and for `smod` (result takes the dividend's sign) this is
/// a correct upper bound. For **signed division `sdiv`** it is NOT: when the
/// dividend is a small non-negative value (narrow width) but the divisor is
/// negative, the quotient is negative — a full-width two's-complement value
/// (~2^256). `sdiv` never marks its result `is_signed`, so the guard misses it.
///
/// `doSdiv` returns `sdiv(x, y)`; every caller passes `and(calldataload,0xff)`
/// so the dividend param `x` narrows, the return min_width is computed as
/// `x`'s width, and the return type is clamped/narrowed — truncating the
/// negative quotient. Four distinct call sites + body size keep the function
/// out of the inliner (CostBenefit -> LLVM noinline), so the narrowed return
/// survives to codegen.
///
/// `sdiv(5, -1) = -5 = 2^256 - 5` on EVM; the narrowed return drops the high
/// bits, returning 0x...fffffffb instead.
object "SdivReturnNarrow" {
code {
let size := datasize("SdivReturnNarrow_deployed")
codecopy(0, dataoffset("SdivReturnNarrow_deployed"), size)
return(0, size)
}
object "SdivReturnNarrow_deployed" {
code {
let z0, w0 := doSdiv(and(calldataload(0), 0xff), calldataload(32))
let z1, w1 := doSdiv(and(calldataload(64), 0xff), calldataload(96))
let z2, w2 := doSdiv(and(calldataload(128), 0xff), calldataload(160))
let z3, w3 := doSdiv(and(calldataload(192), 0xff), calldataload(224))
mstore(0, z0)
mstore(32, w0)
mstore(64, z1)
mstore(96, w1)
mstore(128, z2)
mstore(160, w2)
mstore(192, z3)
mstore(224, w3)
return(0, 256)
function doSdiv(x, y) -> z, w {
z := sdiv(x, y)
// Live chain on the divisor `y` (kept observable via `w`) to
// push the function body over the always-inline size, so the
// narrowed return type reaches codegen.
let a1 := add(y, 1)
let a2 := mul(a1, 3)
let a3 := xor(a2, 7)
let a4 := add(a3, 9)
let a5 := or(a4, 13)
let a6 := add(a5, 17)
let a7 := mul(a6, 19)
let a8 := xor(a7, 23)
w := add(a8, 29)
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
object "SelfCall" {
code { datacopy(0, dataoffset("SelfCall_deployed"), datasize("SelfCall_deployed")) return(0, datasize("SelfCall_deployed")) }
object "SelfCall_deployed" {
code {
let flag := calldataload(0)
let value := calldataload(32)
switch flag
// leaf: echo the value back as return data
case 0 {
mstore(0, value)
return(0, 32)
}
// recurse: build [0, value] calldata, self-call, return the returndata word
default {
mstore(0x80, 0)
mstore(0xA0, value)
let ok := call(gas(), address(), 0, 0x80, 64, 0x100, 32)
// mix in returndatasize and a returndatacopy round-trip
returndatacopy(0x200, 0, returndatasize())
let r := mload(0x100)
r := xor(r, mload(0x200))
r := add(r, ok)
mstore(0, r)
return(0, 32)
}
}
}
}
@@ -0,0 +1,31 @@
object "SelfCallRd" {
code { datacopy(0, dataoffset("SelfCallRd_deployed"), datasize("SelfCallRd_deployed")) return(0, datasize("SelfCallRd_deployed")) }
object "SelfCallRd_deployed" {
code {
let flag := calldataload(0)
switch flag
// leaf: return a chunk of size = calldataload(32) (bounded), filled with calldataload(64)
case 0 {
let sz := and(calldataload(32), 0xFF)
mstore(0, calldataload(64))
mstore(32, not(calldataload(64)))
mstore(64, calldataload(64))
return(0, sz)
}
// caller: self-call leaf, then returndatacopy(dest, off, len) with bounded params
default {
mstore(0x80, 0)
mstore(0xA0, calldataload(32))
mstore(0xC0, calldataload(64))
let ok := call(gas(), address(), 0, 0x80, 96, 0x100, 0)
let rds := returndatasize()
let off := and(calldataload(96), 0xFF)
let len := and(calldataload(128), 0xFF)
returndatacopy(0x200, off, len)
let r := xor(mload(0x200), add(rds, ok))
mstore(0, r)
return(0, 32)
}
}
}
}
@@ -0,0 +1,32 @@
object "ShiftParamNarrow" {
code { datacopy(0, dataoffset("ShiftParamNarrow_deployed"), datasize("ShiftParamNarrow_deployed")) return(0, datasize("ShiftParamNarrow_deployed")) }
object "ShiftParamNarrow_deployed" {
code {
let opx := calldataload(0)
let shamt := calldataload(32)
let xv := calldataload(64)
let r0 := f(opx, shamt, xv)
let r1 := f(opx, shamt, xv)
let r2 := f(opx, shamt, xv)
let r3 := f(opx, shamt, xv)
mstore(0x200, r0)
mstore(0x220, add(r1, add(r2, r3)))
return(0x200, 64)
function f(op, s, v) -> r {
switch op
case 0 { r := mload(s) } // narrows s to i64
case 1 { r := shl(s, v) }
case 2 { r := shr(s, v) }
case 3 { r := sar(s, v) }
case 4 {
// bulk to push f over the inline threshold (keep f a real function)
mstore(0x300, s) mstore(0x320, v) mstore(0x340, op)
r := keccak256(0x300, 0x60)
r := add(r, keccak256(0x300, 0x40))
r := add(r, keccak256(0x300, 0x20))
}
default { r := s }
}
}
}
}
@@ -0,0 +1,19 @@
object "SignedCmpNarrow" {
code { datacopy(0, dataoffset("SignedCmpNarrow_deployed"), datasize("SignedCmpNarrow_deployed")) return(0, datasize("SignedCmpNarrow_deployed")) }
object "SignedCmpNarrow_deployed" {
code {
let op := calldataload(0)
let a := calldataload(32)
let b := calldataload(64)
let r := 0
switch op
// i1 operands: sgt(0, 1) must be 0 (EVM), not 1
case 0 { r := sgt(eq(a, b), gt(a, b)) }
case 1 { r := slt(gt(a, b), eq(a, b)) }
// i8 operands with top bit set: sgt(0xC8, 0x05) must be 1 (both positive)
case 2 { r := sgt(and(a, 0xFF), and(b, 0xFF)) }
case 3 { r := slt(and(a, 0xFF), and(b, 0xFF)) }
mstore(0, r) return(0, 32)
}
}
}
@@ -0,0 +1,28 @@
/// Probe: if newyork narrows the signextend BYTE-INDEX parameter to i64, a huge
/// index (>=31, which EVM treats as "leave value unchanged") truncated to a
/// small index would sign-extend from a low byte instead, changing the value.
/// `pp` is used as mload offset (case 0) to drive narrowing, and as the
/// signextend index (case 1). value 0x80 has bit 7 set, so signextend(0, 0x80)
/// = 0xff..ff80 while signextend(huge, 0x80) = 0x80.
object "SignextendIndexNarrow" {
code { let s := datasize("SignextendIndexNarrow_deployed") codecopy(0, dataoffset("SignextendIndexNarrow_deployed"), s) return(0, s) }
object "SignextendIndexNarrow_deployed" {
code {
let op := calldataload(0)
let p := calldataload(32)
let r0 := probe(op, p)
let r1 := probe(op, p)
let r2 := probe(op, p)
let r3 := probe(op, p)
mstore(0x200, r0) mstore(0x220, add(r1, add(r2, r3)))
return(0x200, 64)
function probe(opx, pp) -> r {
switch opx
case 0 { r := mload(pp) }
case 1 { r := signextend(pp, 0x80) }
case 2 { r := keccak256(pp, 32) }
default { r := pp }
}
}
}
}
@@ -0,0 +1,34 @@
object "StorageProbe" {
code {
datacopy(0, dataoffset("StorageProbe_deployed"), datasize("StorageProbe_deployed"))
return(0, datasize("StorageProbe_deployed"))
}
object "StorageProbe_deployed" {
code {
let k := calldataload(0)
let v := calldataload(32)
// >= 9 mapping-style sstores in the `let h := keccak256(..); sstore(h, _)`
// form so compound_outlining's keccak256_pair + sstore -> mapping_sstore
// fusion (T9, total mapping ops >= 9) triggers. Distinct base slots so
// the slots do not collide. The differential runner compares the full
// storage state between newyork-PVM and solc-EVM.
mstore(0, k) mstore(0x20, 0) let s0 := keccak256(0, 64) sstore(s0, v)
mstore(0, k) mstore(0x20, 1) let s1 := keccak256(0, 64) sstore(s1, add(v, 1))
mstore(0, k) mstore(0x20, 2) let s2 := keccak256(0, 64) sstore(s2, not(v))
mstore(0, k) mstore(0x20, 3) let s3 := keccak256(0, 64) sstore(s3, shl(200, v))
mstore(0, k) mstore(0x20, 4) let s4 := keccak256(0, 64) sstore(s4, and(v, 0xFF))
mstore(0, add(k, 1)) mstore(0x20, 0) let s5 := keccak256(0, 64) sstore(s5, v)
mstore(0, add(k, 2)) mstore(0x20, 0) let s6 := keccak256(0, 64) sstore(s6, sub(0, v))
mstore(0, not(k)) mstore(0x20, 7) let s7 := keccak256(0, 64) sstore(s7, v)
mstore(0, k) mstore(0x20, 8) let s8 := keccak256(0, 64) sstore(s8, v)
mstore(0, k) mstore(0x20, 9) let s9 := keccak256(0, 64) sstore(s9, exp(v, 3))
// also read one back through the fused mapping_sload path and store it plainly
mstore(0, k) mstore(0x20, 0) let r := sload(keccak256(0, 64))
sstore(0xFFFF, r)
return(0, 0)
}
}
}
@@ -0,0 +1,11 @@
object "Store8Fmp" {
code { datacopy(0, dataoffset("Store8Fmp_deployed"), datasize("Store8Fmp_deployed")) return(0, datasize("Store8Fmp_deployed")) }
object "Store8Fmp_deployed" {
code {
mstore8(0x40, 0xAB)
let r := mload(0x40)
mstore(0, r)
return(0, 32)
}
}
}
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// Regression test: ABI parameter validation for sub-256-bit types.
// Solidity 0.8+ reverts when a calldataload value exceeds the range of its
// declared type (e.g., passing 256 to a uint8 parameter).
// Bug: the newyork pipeline silently truncates instead of reverting.
/* runner.json
{
"differential": true,
"actions": [
{
"Instantiate": {
"code": { "Solidity": { "contract": "SubTypeValidation", "solc_optimizer": false } }
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "d552145b0000000000000000000000000000000000000000000000000000000100000000"
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "7440c86b0000000000000000000000000000000000000000000000010000000000000000"
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "0850cee20000000000000000000000000000000100000000000000000000000000000000"
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "d552145b0000000000000000000000000000000000000000000000000000000000000005"
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "7440c86b0000000000000000000000000000000000000000000000000000000000000005"
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "0850cee20000000000000000000000000000000000000000000000000000000000000005"
}
}
]
}
*/
contract SubTypeValidation {
// Should revert when called with value > 0xFF
function narrow_uint8(uint8 x) public pure returns (uint8) {
return x;
}
// Should revert when called with value > 0xFFFFFFFFFFFFFFFF
function narrow_uint64(uint64 x) public pure returns (uint64) {
return x;
}
// Should revert when called with value > 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
function narrow_uint128(uint128 x) public pure returns (uint128) {
return x;
}
}
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// Regression test: sub-underflow result widened to uint256 must preserve EVM
// modular semantics.
//
// Bug (crates/newyork/src/to_llvm.rs ~5977-5999): when `BinaryOperation::Sub`
// is codegen'd with both operands LLVM-narrowed to <= 64 bits (via
// `try_narrow_let_binding` Strategy 1 — structural proof from `and value, mask`)
// and the consumer's demand for the let binding holding the sub result is None
// (here: the result is mstored as a uint256), codegen takes the i128 fast-path:
// sub at i128 of (0, 1) → 2^128 - 1 (high bit set)
// Later widening via ensure_word_type → build_int_z_extend produces
// 0x...0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF instead of 2^256 - 1.
//
// Inline assembly is used so the masks land as IR constants (Solidity's
// non-asm path wraps integer literals through a `convert_rational_*` function,
// which defeats the structural narrowness proof).
/* runner.json
{
"differential": true,
"actions": [
{
"Instantiate": {
"code": { "Solidity": { "contract": "SubUnderflowZext", "solc_optimizer": false } }
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "0aa7a67700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
}
}
]
}
*/
contract SubUnderflowZext {
function sub_underflow(uint256 a, uint256 b) external pure returns (uint256 r) {
assembly {
let x := and(a, 0xff)
let y := and(b, 0xff)
r := sub(x, y)
}
}
}
@@ -0,0 +1,17 @@
object "SwitchCvCseDangling" {
code { let s := datasize("SwitchCvCseDangling_deployed") codecopy(0, dataoffset("SwitchCvCseDangling_deployed"), s) return(0, s) }
object "SwitchCvCseDangling_deployed" {
code {
let sel := calldataload(0)
// Every branch (cases + default) starts with an equivalent non-payable callvalue check, so the
// check is fully hoisted above the switch and dropped from each branch. The cases also read
// `callvalue()` a SECOND time; environment CSE rewrites that read to reuse the first binding.
// Hoisting must redirect that reuse to the hoisted binding, otherwise it references the
// dropped definition and trips the SSA validator.
switch sel
case 1 { let cv := callvalue() if cv { revert(0, 0) } let x := callvalue() sstore(0, add(x, 0x100)) }
case 2 { let cv := callvalue() if cv { revert(0, 0) } let y := callvalue() sstore(0, add(y, 0x200)) }
default { let cv := callvalue() if cv { revert(0, 0) } sstore(0, 0xdd) }
}
}
}
@@ -0,0 +1,17 @@
object "SwitchCvFallthrough" {
code { let s := datasize("SwitchCvFallthrough_deployed") codecopy(0, dataoffset("SwitchCvFallthrough_deployed"), s) return(0, s) }
object "SwitchCvFallthrough_deployed" {
code {
let sel := calldataload(0)
switch sel
// Each selector is non-payable: it reverts if value was sent.
case 1 { let cv := callvalue() if cv { revert(0, 0) } sstore(0, 0x11) return(0, 0) }
case 2 { let cv := callvalue() if cv { revert(0, 0) } sstore(0, 0x22) return(0, 0) }
// There is NO default. A no-match selector must fall through to here. Hoisting the cases'
// callvalue check above the switch would make this fall-through path revert on a nonzero
// value, even though it never had a callvalue check of its own.
sstore(0, 0xff)
return(0, 0)
}
}
}
@@ -0,0 +1,13 @@
/// Force scrutinee to i64 via an explicit 64-bit mask.
object "SwitchWideLabel" {
code { datacopy(0, dataoffset("SwitchWideLabel_deployed"), datasize("SwitchWideLabel_deployed")) return(0, datasize("SwitchWideLabel_deployed")) }
object "SwitchWideLabel_deployed" {
code {
let x := and(calldataload(0), 0xffffffffffffffff)
switch x
case 0 { mstore(0, 0xAA) return(0, 32) }
case 0x10000000000000000 { mstore(0, 0xBB) return(0, 32) }
default { mstore(0, 0xCC) return(0, 32) }
}
}
}
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
/// Regression reproducer for a newyork SSA-validation ICE that surfaced only with
/// the solc optimizer disabled (`--disable-solc-optimizer`).
///
/// `try ... catch { return <constant>; }` lowers (with the solc optimizer off) to a
/// switch whose default region is `let r := if (true) { ...; leave } else { ... }`
/// followed by `yield r`. The inliner's leave-elimination produces the
/// constant-condition `if` with an output; the simplifier folds it, appending the
/// output binding after the branch's `leave`. The dead-code pass then truncated
/// everything after that terminator — including the binding the `yield` still
/// referenced — leaving `yield r` dangling ("value used before definition").
///
/// Both switch arms `leave`, so the yielded fall-through value is provably never
/// observed; the fix zero-binds the rescued value before the terminator so SSA
/// stays valid. Instantiated with the solc optimizer disabled to hit the path.
contract TryCatchCatchReturn {
function boom() external pure returns (uint256) {
revert("boom");
}
function run() public returns (uint256) {
try this.boom() returns (uint256 value) {
return value;
} catch {
return 200;
}
}
}
@@ -0,0 +1,19 @@
object "TstoreProbe" {
code { datacopy(0, dataoffset("TstoreProbe_deployed"), datasize("TstoreProbe_deployed")) return(0, datasize("TstoreProbe_deployed")) }
object "TstoreProbe_deployed" {
code {
let a := calldataload(0)
let b := calldataload(32)
let r := 0
tstore(a, b)
tstore(add(a, 1), not(b))
r := xor(r, tload(a))
r := xor(r, tload(add(a, 1)))
r := xor(r, tload(add(a, 2)))
tstore(a, and(b, 0xFF))
r := add(r, tload(a))
mstore(0, r)
return(0, 32)
}
}
}
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// Regression test: arithmetic with uint128 types.
// Bug: newyork pipeline produces wrong results for uint128 arithmetic with
// values that trigger overflow checking in Solidity 0.8+.
/* runner.json
{
"differential": true,
"actions": [
{
"Instantiate": {
"code": { "Solidity": { "contract": "Uint128Arithmetic", "solc_optimizer": false } }
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "ba517410000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000100000000000000000000000000000000"
}
},
{
"Call": {
"dest": { "Instantiated": 0 },
"data": "ba51741000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001f"
}
}
]
}
*/
contract Uint128Arithmetic {
uint128 constant EPS = 1E10;
uint128 constant PRECISION = 100;
uint128 constant MAX_U128_SQRT = 18446744073709551615;
function entry(uint128 a, uint128 mb, uint128 c) public pure returns (bool, uint128, uint128) {
(bool p, uint128 x1, uint128 x2) = main(a, mb, c);
x1 *= PRECISION;
x1 /= EPS;
x2 *= PRECISION;
x2 /= EPS;
return (p, x1, x2);
}
function sqrt(uint128 n) private pure returns (uint128) {
uint128 l = 0;
uint128 r = MAX_U128_SQRT;
while (l < r) {
uint128 m = (l + r + 1) / 2;
if (m * m <= n) {
l = m;
} else {
r = m - 1;
}
}
if (n - l * l < (l + 1) * (l + 1) - n) {
return l;
} else {
return l + 1;
}
}
function main(uint128 a, uint128 mb, uint128 c) private pure returns (bool, uint128, uint128) {
if (mb * mb < 4 * a * c) {
return (false, 0, 0);
}
uint128 d = (mb * mb - 4 * a * c) * EPS * EPS;
uint128 sd = sqrt(d);
uint128 x1 = (mb * EPS + sd) / 2 / a;
uint128 x2 = (mb * EPS - sd) / 2 / a;
return (true, x1, x2);
}
}
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: `mem_opt::optimize_statements` MStore8 handler only
/// invalidates `memory_state[word_offset(offset)]`. An unaligned
/// `mstore(0x70, AA…)` leaves a tracked entry at word 0x60 with
/// `tracked.offset = 0x70`, whose 32-byte write range covers byte
/// 0x80. A later `mstore8(0x80, 0xCC)` overwrites that byte but only
/// removes `memory_state[0x80]`, not the overlapping entry at
/// `memory_state[0x60]`. A subsequent `mload(0x70)` matches the
/// stale tracked entry by exact-offset comparison and is forwarded
/// to the pre-overwrite value, dropping the single-byte overwrite.
/// EVM observes byte 0x80 = `0xCC` in the actual memory read.
contract UnalignedMStore8Bug {
function bug() external pure returns (bytes32 r) {
assembly {
mstore(0x70, 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)
mstore8(0x80, 0xcc)
r := mload(0x70)
}
}
}
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
/// Soundness PoC: `mem_opt.rs` only updates `memory_state[word_offset]` when
/// recording an mstore, where `word_offset = static_offset / 32 * 32`. An
/// *unaligned* `mstore(0x70, v)` writes 32 bytes from byte 0x70 to 0x90, so
/// it overwrites the lower half of the word starting at 0x80. mem_opt only
/// touches `memory_state[0x60]` and leaves `memory_state[0x80]` intact.
///
/// A subsequent `mload(0x80)` finds the still-cached tracked entry at 0x80
/// (offset == 0x80, no aliasing check across overlap with 0x70) and is
/// forwarded to the *pre-overwrite* stored value. EVM reads the actual
/// memory which contains the lower half of the unaligned store plus the
/// untouched upper half of the original.
contract UnalignedMStoreBug {
function bug() external pure returns (bytes32 r) {
assembly {
// 32 byte sentinel at the aligned word 0x80.
mstore(0x80, 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)
// Unaligned write: 16 bytes of 0xbb at 0x70..0x80 and 16 bytes of
// 0xcc at 0x80..0x90 (overwriting the lower half of the sentinel).
mstore(0x70, 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc)
// mem_opt forwards the stale sentinel, missing the partial overwrite.
r := mload(0x80)
}
}
}
+218 -1
View File
@@ -179,12 +179,222 @@ sol!(
function mod(uint n, uint d) public pure returns (uint r);
function smod(int n, int d) public pure returns (int r);
function divSelf(uint256 x) external pure returns (uint256 r);
function sdivSelf(int256 x) external pure returns (int256 r);
function modSelf(uint256 x) external pure returns (uint256 r);
}
);
case!("DivisionArithmetics.sol", DivisionArithmetics, divCall, division_arithmetics_div, n: U256, d: U256);
case!("DivisionArithmetics.sol", DivisionArithmetics, sdivCall, division_arithmetics_sdiv, n: I256, d: I256);
case!("DivisionArithmetics.sol", DivisionArithmetics, modCall, division_arithmetics_mod, n: U256, d: U256);
case!("DivisionArithmetics.sol", DivisionArithmetics, smodCall, division_arithmetics_smod, n: I256, d: I256);
case!("DivisionArithmetics.sol", DivisionArithmetics, divSelfCall, division_arithmetics_div_self, x: U256);
case!("DivisionArithmetics.sol", DivisionArithmetics, sdivSelfCall, division_arithmetics_sdiv_self, x: I256);
case!("DivisionArithmetics.sol", DivisionArithmetics, modSelfCall, division_arithmetics_mod_self, x: U256);
sol!(
contract SDivNarrowBug {
function sdiv_masked(uint256 a, uint256 b) external pure returns (int256 q);
}
);
case!("SDivNarrowBug.sol", SDivNarrowBug, sdiv_maskedCall, sdiv_narrow_bug_masked, a: U256, b: U256);
sol!(
contract KeccakFuseBug {
function probe(uint256[8] calldata seeds) external view returns (uint256 r, bytes32 sink_out);
}
);
case!(
"KeccakFuseBug.sol",
KeccakFuseBug,
probeCall,
keccak_fuse_bug_probe,
seeds: [U256; 8]
);
sol!(
contract ParamMload {
function tryFetch(uint256 x) external pure returns (uint256);
}
);
case!("ParamMload.sol", ParamMload, tryFetchCall, param_mload_try_fetch, x: U256);
// `PanicCodeBug` has only a `fallback()` — invoked by sending raw
// calldata that doesn't match any function selector. Empty calldata
// suffices.
case!(
"PanicCodeBug.sol",
"PanicCodeBug",
vec![],
panic_code_bug_trigger
);
// `FmpDynStoreBug` is invoked via empty fallback, passing 0x40 as the
// only calldata word so the inline-asm `calldataload(0)` resolves to
// 0x40 at runtime but stays opaque to the simplifier.
case!(
"FmpDynStoreBug.sol",
"FmpDynStoreBug",
{
let mut bytes = vec![0u8; 32];
bytes[31] = 0x40;
bytes
},
fmp_dyn_store_bug
);
// `FmpRangeProofBug` reads the 32-byte calldata as the value to put
// into the FMP slot. Use `0x100000007 = 2^32 + 7` (well beyond the
// 17-bit heap-size range) to make the range-proof truncation visible.
case!(
"FmpRangeProofBug.sol",
"FmpRangeProofBug",
{
let mut bytes = vec![0u8; 32];
bytes[31] = 0x07;
bytes[27] = 0x01;
bytes
},
fmp_range_proof_bug
);
// `FmpNativeStoreBug` calldata: first word = FMP value `0x100000007`,
// second word = non-zero condition to force the if-branch.
case!(
"FmpNativeStoreBug.sol",
"FmpNativeStoreBug",
{
let mut bytes = vec![0u8; 64];
bytes[31] = 0x07;
bytes[27] = 0x01;
bytes[63] = 0x01;
bytes
},
fmp_native_store_bug
);
// `FmpCrossObjectBug` calldata: first word = FMP value `0x100000007`,
// second word = recursion depth (0 to hit the inner inline-asm branch).
case!(
"FmpCrossObjectBug.sol",
"FmpCrossObjectBug",
{
let mut bytes = vec![0u8; 64];
bytes[31] = 0x07;
bytes[27] = 0x01;
bytes
},
fmp_cross_object_bug
);
// `FmpRevertBug` is invoked via empty calldata into its `fallback()`.
case!("FmpRevertBug.sol", "FmpRevertBug", vec![], fmp_revert_bug);
// `FmpDynRevertBug` reads `offset` and `length` from calldata: pass
// 64 bytes encoding `offset=0`, `length=96` so the dynamic revert
// covers the FMP slot.
case!(
"FmpDynRevertBug.sol",
"FmpDynRevertBug",
{
let mut bytes = vec![0u8; 64];
bytes[63] = 96;
bytes
},
fmp_dyn_revert_bug
);
sol!(
contract UnalignedMStoreBug {
function bug() external pure returns (bytes32);
}
);
case!(
"UnalignedMStoreBug.sol",
UnalignedMStoreBug,
bugCall,
unaligned_mstore_bug,
);
sol!(
contract ConstReturnOverflowBug {
function bug() external pure returns (uint256);
}
);
case!(
"ConstReturnOverflowBug.sol",
ConstReturnOverflowBug,
bugCall,
const_return_overflow_bug,
);
// `PanicInterveneBug` has only a `fallback()` — invoked by sending
// raw calldata that doesn't match any selector. Empty calldata works.
case!(
"PanicInterveneBug.sol",
"PanicInterveneBug",
vec![],
panic_intervene_bug
);
sol!(
contract UnalignedMStore8Bug {
function bug() external pure returns (bytes32);
}
);
case!(
"UnalignedMStore8Bug.sol",
UnalignedMStore8Bug,
bugCall,
unaligned_mstore8_bug,
);
sol!(
contract CopyOverlapBug {
function bug(uint256 length) external pure returns (bytes32);
}
);
case!(
"CopyOverlapBug.sol",
CopyOverlapBug,
bugCall,
copy_overlap_bug,
length: U256
);
sol!(
contract CallerOriginAliasing {
function caller_then_origin() external view returns (address, address);
function origin_then_caller() external view returns (address, address);
function caller_address_origin() external view returns (address, address, address);
function repeated_caller() external view returns (address, address, address);
}
);
case!(
"CallerOriginAliasing.sol",
CallerOriginAliasing,
caller_then_originCall,
caller_origin_aliasing_caller_then_origin,
);
case!(
"CallerOriginAliasing.sol",
CallerOriginAliasing,
origin_then_callerCall,
caller_origin_aliasing_origin_then_caller,
);
case!(
"CallerOriginAliasing.sol",
CallerOriginAliasing,
caller_address_originCall,
caller_origin_aliasing_caller_address_origin,
);
case!(
"CallerOriginAliasing.sol",
CallerOriginAliasing,
repeated_callerCall,
caller_origin_aliasing_repeated_caller,
);
sol!(
contract DivConst {
@@ -293,6 +503,8 @@ sol!(
contract ExtCode {
function ExtCodeSize(address who) public view returns (uint ret);
function ExtCodeSizeSum(address a, address b) public view returns (uint ret);
function CodeSize() public pure returns (uint ret);
function ExtCodeHash(address who) public view returns (bytes32 ret);
@@ -301,6 +513,7 @@ sol!(
}
);
case!("ExtCode.sol", ExtCode, ExtCodeSizeCall, ext_code_size, address: Address);
case!("ExtCode.sol", ExtCode, ExtCodeSizeSumCall, ext_code_size_sum, a: Address, b: Address);
case!("ExtCode.sol", ExtCode, CodeSizeCall, code_size,);
case!("ExtCode.sol", ExtCode, ExtCodeHashCall, ext_code_hash, address: Address);
case!("ExtCode.sol", ExtCode, CodeHashCall, code_hash,);
@@ -388,7 +601,11 @@ mod tests {
#[test]
fn codesize() {
let path = "codesize.json";
let path = if cfg!(feature = "newyork") {
"codesize_newyork.json"
} else {
"codesize.json"
};
let existing = File::open(path)
.map(|file| {
File diff suppressed because it is too large. Load diff