1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! Property tests for the constructor / deployment lifecycle.
//!
//! Wave-#30's smoke pass surfaced 14 example contracts that fault with
//! `"PICKITEM: unsupported target Null"` when a view method is invoked
//! WITHOUT the `_deploy` prologue having run — those are whitelisted as
//! known-acceptable graceful exceptions, but the side-effect is that the
//! constructor's storage-init logic (declaration initializers + ctor body)
//! is the most complex code path in real contracts and is also the path
//! that smoke coverage skips. This module closes the gap: every test here
//! invokes a method via `call_method` / `call_method_with_deploy_args`, so
//! the runtime's `_deploy(data, false)` prologue runs (see
//! `src/runtime/runtime_parts/runtime_impl/runtime/execution.rs:158-180`
//! for the auto-fire site) before any view method observes storage.
//!
//! Five proptests:
//!
//! a. `constructor_storage_init_visible_post_deploy` — declaration-init
//! (`uint256 public counter = 7`) AND ctor-body init (`owner =
//! msg.sender`) together. Asserts both land on the right slots.
//!
//! b. `constructor_with_args_storage_init` — `constructor(uint256 _seed,
//! address _admin)` with random fuzzed values, threaded through
//! `call_method_with_deploy_args(Some(&[..]))`. Asserts the args
//! survived `_deploy`'s `jsonDeserialize` / `deserialize` decoding
//! pipeline (see `src/ir/ir_deploy.rs:143+`) and landed on the named
//! slots.
//!
//! c. `constructor_payable_accepts_value` — `payable constructor`
//! reads `msg.value` set via `rt.override_value(N)`. Strategy A
//! fix in `call_method_with_deploy_args`: the deploy-snapshot
//! block continues to save/restore `pending_timestamp` /
//! `pending_block_height` / `pending_caller_account` across the
//! implicit `_deploy` call, but `pending_msg_value` is allowed to
//! drain into the deploy prologue so the constructor frame sees
//! the override. Asserts `receivedValue == override_value(N)`.
//!
//! d. `constructor_runs_only_once` — Counter contract, ctor sets counter
//! to 1; we call the view 5 times. Asserts the counter is 1 every
//! time — `_deploy` does NOT re-run on each user call. The runtime
//! gates this on `self.deploy_triggered` (set true after the first
//! successful `_deploy` invocation in `call_method_with_deploy_args`,
//! see `execution.rs:334`).
//!
//! e. `constructor_with_modifier` — modifier-on-constructor inlining.
//! `modifier nonZero(uint256 x) { require(x > 0, "zero"); _; }
//! constructor(uint256 _seed) nonZero(_seed) { seed = _seed; }`. With
//! a non-zero seed, the modifier passes through and the ctor body
//! runs; we read the seed back via the public auto-getter. (We
//! cannot exercise the failing-modifier branch via this entry point
//! because if `_deploy` reverts, `call_method_with_deploy_args`
//! bubbles it up before the user method runs, so the seed-zero case
//! is not separable from a generic deploy failure — out of scope.)
#![allow(unused_imports)]
use super::common::*;
use neo_devpack_solidity::cli::compile_contracts;
use neo_devpack_solidity::runtime::types::StackItem;
use neo_devpack_solidity::runtime::{NeoRuntime, RuntimeConfig};
use num_bigint::BigUint;
use proptest::prelude::*;
proptest! {
// 8-16 cases per test — each compiles a small contract, deploys, and
// reads 1-2 view methods. Bound at <100ms/case.
#![proptest_config(ProptestConfig::with_cases(8))]
/// **a. Declaration-init + ctor-body init both visible after deploy.**
///
/// `uint256 public counter = 7;` is a declaration-side initializer that
/// the compiler wires into the `_deploy` prologue's pre-constructor
/// init block (see `src/ir/ir_deploy.rs:90-105`). `address public
/// owner;` is uninitialised at declaration; the ctor body assigns
/// `owner = msg.sender`. After `_deploy` runs, BOTH slots must be
/// populated and reachable by their auto-getters.
///
/// Failure modes (each maps to a real bug):
/// - counter() returns 0: declaration-init dropped on the floor —
/// `lower_expression` / `StoreState` regression in ir_deploy.
/// - owner() returns all-zero bytes: ctor body never ran, OR
/// `msg.sender` inside the deploy frame returned zero (Task #176
/// regression — caller-account didn't propagate).
#[test]
fn constructor_storage_init_visible_post_deploy(_seed in any::<u8>()) {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
uint256 public counter = 7;
address public owner;
constructor() {
owner = msg.sender;
}
}"#;
let arts = compile_contracts(src, false, 2)
.unwrap_or_else(|e| panic!("ctor-lifecycle (a) compile: {:?}", e));
prop_assert!(!arts.is_empty(), "ctor-lifecycle (a) produced no artifacts");
let art = &arts[0];
// Pin a deterministic deployer so msg.sender is reproducible across
// proptest cases. 0xAA*20 is the convention used by the openzeppelin
// and contract-upgrade fuzz suites.
let deployer: [u8; 20] = [0xAA; 20];
let deployer_hex = format!("0x{}", hex::encode(deployer));
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("rt");
rt.override_caller_account(&deployer_hex)
.expect("deployer override");
// Read counter() — auto-getter for the declaration-initialised slot.
let r_c = rt.call_method(
&art.bytecode, &art.tokens, &art.manifest, "counter", &[],
).expect("counter() host-level (a)");
prop_assert!(r_c.success,
"counter() must succeed post-deploy; exc={:?}. If this is a \
PICKITEM-Null-target fault, `_deploy` did not auto-fire and \
the slot was never written — call_method's deploy-trigger \
gating regressed.",
r_c.exception.as_ref().map(|e| &e.message));
let counter_val = decode_uint_le(&r_c.return_data);
prop_assert_eq!(counter_val.clone(), BigUint::from(7u64),
"counter() must equal 7 (declaration-side init); got {} \
(rd_hex={}). If 0, the declaration initializer was dropped \
from the `_deploy` prologue — `ir_deploy.rs::lower_expression` \
/ `StoreState` emission regressed for declaration-init.",
counter_val, hex::encode(&r_c.return_data));
// Read owner() — must contain the deployer's bytes (0xAA).
let r_o = rt.call_method(
&art.bytecode, &art.tokens, &art.manifest, "owner", &[],
).expect("owner() host-level (a)");
prop_assert!(r_o.success,
"owner() must succeed post-deploy; exc={:?}.",
r_o.exception.as_ref().map(|e| &e.message));
prop_assert!(r_o.return_data.iter().any(|b| *b == 0xAA),
"owner() must contain deployer bytes (0xAA repeating) since \
the ctor body executed `owner = msg.sender`; got rd_hex={}. \
If alice-free, either (1) the ctor body never ran (deploy \
prologue regression), or (2) msg.sender inside `_deploy` did \
not equal the host's overridden caller (Task #176 sticky \
override didn't propagate into the deploy frame).",
hex::encode(&r_o.return_data));
}
/// **b. Constructor with args — uint256 _seed, address _admin.**
///
/// `call_method_with_deploy_args` wraps `Some(&[..])` into a
/// `StackItem::Array` and passes it as `data` to `_deploy(data,
/// update)`. The compiled prologue then attempts `jsonDeserialize`
/// (throws on Array — caught), `deserialize` (throws on Array —
/// caught), and finally falls through to the original `data` Array;
/// PICKITEM(0)/PICKITEM(1) extract the constructor's positional args
/// (see `src/ir/ir_deploy.rs:143-220`).
///
/// We fuzz the seed (u32-bounded so the LE-decoder yields a tractable
/// BigUint) and a 20-byte admin address. Both must be reachable by
/// their public auto-getters post-deploy.
#[test]
fn constructor_with_args_storage_init(
seed in 1u64..=1_000_000u64,
admin_bytes in any::<[u8; 20]>(),
) {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
uint256 public seed;
address public admin;
constructor(uint256 _seed, address _admin) {
seed = _seed;
admin = _admin;
}
}"#;
let arts = compile_contracts(src, false, 2)
.unwrap_or_else(|e| panic!("ctor-lifecycle (b) compile: {:?}", e));
prop_assert!(!arts.is_empty(), "ctor-lifecycle (b) produced no artifacts");
let art = &arts[0];
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("rt");
// Deploy with the fuzzed args. The first method call (seed())
// triggers `_deploy(data=[seed, admin], update=false)`.
let r_seed = rt.call_method_with_deploy_args(
&art.bytecode, &art.tokens, &art.manifest,
"seed", &[] as &[StackItem],
Some(&[
StackItem::UnsignedInteger(seed),
StackItem::byte_array(admin_bytes.to_vec()),
]),
).expect("seed() with deploy_args host-level (b)");
prop_assert!(r_seed.success,
"seed() post-parameterised-deploy must succeed; exc={:?}. \
If this is a 'PICKITEM: unsupported target Null', the deploy \
args Array did not survive `_deploy`'s jsonDeserialize / \
deserialize fallback chain — check `ir_deploy.rs` ctor-arg \
extraction.",
r_seed.exception.as_ref().map(|e| &e.message));
let got_seed = decode_uint_le(&r_seed.return_data);
prop_assert_eq!(got_seed.clone(), BigUint::from(seed),
"seed() must equal the fuzzed _seed value {}; got {} \
(rd_hex={}). Mismatch = ctor-arg ordering or extraction \
regression: PICKITEM(0) read a different slot than the \
constructor body wrote.",
seed, got_seed, hex::encode(&r_seed.return_data));
// admin() — readback the second ctor arg.
let r_admin = rt.call_method(
&art.bytecode, &art.tokens, &art.manifest, "admin", &[],
).expect("admin() host-level (b)");
prop_assert!(r_admin.success,
"admin() must succeed; exc={:?}.",
r_admin.exception.as_ref().map(|e| &e.message));
// The address bytes should appear in the return data (the runtime
// may emit them with leading-zero stripping or a fixed 20-byte
// width — we assert presence rather than exact match to absorb
// both shapes, mirroring openzeppelin_patterns_props's pattern).
let admin_present = admin_bytes.iter().any(|b| *b != 0)
&& r_admin.return_data
.windows(admin_bytes.len())
.any(|w| w == &admin_bytes[..]);
// For all-zero admin bytes, the readback is also zero, which
// matches both an unwritten slot AND a written-zero-address slot
// — indistinguishable at this layer. Skip the strict check in
// that case (proptest's `any::<[u8;20]>()` will exercise the
// non-zero path on the vast majority of cases).
if admin_bytes.iter().any(|b| *b != 0) {
prop_assert!(admin_present,
"admin() must contain the fuzzed admin bytes {:?}; got \
rd_hex={}. Mismatch = PICKITEM(1) read the wrong slot \
or the address-typed ctor arg was decoded under a \
different shape than it was passed.",
admin_bytes, hex::encode(&r_admin.return_data));
}
}
/// **c. Payable constructor — `msg.value` flows in via `override_value`.**
///
/// Solidity `constructor() payable { receivedValue = msg.value; }`
/// expects `msg.value` to be visible inside the constructor frame.
/// `call_method_with_deploy_args` lets `_deploy` consume
/// `pending_msg_value` (see `execution.rs` Task #113 Strategy A
/// note around the deploy snapshot site): the timestamp /
/// block_height / caller_account override slots are still
/// snapshot-and-restored across `_deploy` so a user-facing method
/// observes them, but `pending_msg_value` is allowed to drain into
/// the deploy prologue so a `payable constructor` reads the value
/// the harness set via `rt.override_value(N)`. The asserted
/// invariant: `receivedValue` after deploy equals the override.
#[test]
fn constructor_payable_accepts_value(value in 1u64..=1_000_000u64) {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
uint256 public receivedValue;
constructor() payable {
receivedValue = msg.value;
}
}"#;
let arts = compile_contracts(src, false, 2)
.unwrap_or_else(|e| panic!("ctor-lifecycle (c) compile: {:?}", e));
prop_assert!(!arts.is_empty(), "ctor-lifecycle (c) produced no artifacts");
let art = &arts[0];
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("rt");
rt.override_value(value);
let r = rt.call_method(
&art.bytecode, &art.tokens, &art.manifest, "receivedValue", &[],
).expect("receivedValue() host-level (c)");
prop_assert!(r.success,
"receivedValue() must succeed; exc={:?}.",
r.exception.as_ref().map(|e| &e.message));
let got = decode_uint_le(&r.return_data);
// Neo N3 has no EVM-style attached call value: `msg.value` is always 0
// (received amounts arrive via onNEP17Payment, not an ambient msg.value),
// so a payable constructor reading msg.value observes 0 regardless of the
// injected `override_value`. This keeps the emitted bytecode conformant
// (no fabricated GetMsgValue syscall that faults on a real node).
let _ = value;
prop_assert_eq!(got.clone(), BigUint::from(0u8),
"payable-ctor msg.value must be 0 on Neo (no attached value); got {}",
got);
}
/// **d. Constructor runs exactly once — view called 5 times.**
///
/// `_deploy` is gated by `self.deploy_triggered` (set true at
/// `execution.rs:334` after the first successful invocation). Every
/// subsequent `call_method` on the same `NeoRuntime` skips the
/// `_deploy` invocation, so a constructor that initialises `counter
/// = 1` must NOT re-run and reset the counter on each call — and a
/// view method that reads the counter must therefore observe `1`
/// every time, regardless of how many times we ask.
///
/// (We cannot directly assert "did NOT re-run" via a side effect
/// because Neo's runtime is deterministic — there's no monotonic
/// counter incremented by `_deploy` itself. But: if `_deploy` did
/// re-run, the storage write `counter = 1` would still be idempotent.
/// We therefore use a pattern where the contract uses an
/// always-stable initial value (1) — what we verify is the lighter
/// invariant that view reads remain consistent across repeated
/// `call_method` invocations. The deploy-once gate is also pinned by
/// the existing `deploy_triggered` flag in `execution.rs`.)
#[test]
fn constructor_runs_only_once(_seed in any::<u8>()) {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
uint256 public counter;
constructor() {
counter = 1;
}
}"#;
let arts = compile_contracts(src, false, 2)
.unwrap_or_else(|e| panic!("ctor-lifecycle (d) compile: {:?}", e));
prop_assert!(!arts.is_empty(), "ctor-lifecycle (d) produced no artifacts");
let art = &arts[0];
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("rt");
// Five back-to-back view reads on the same `NeoRuntime`. Each
// must return 1; the first triggers `_deploy`, the next four
// must skip it (deploy_triggered gate).
for i in 0..5 {
let r = rt.call_method(
&art.bytecode, &art.tokens, &art.manifest, "counter", &[],
).expect("counter() host-level (d)");
prop_assert!(r.success,
"counter() call #{} must succeed; exc={:?}.",
i, r.exception.as_ref().map(|e| &e.message));
let got = decode_uint_le(&r.return_data);
prop_assert_eq!(got.clone(), BigUint::from(1u64),
"counter() call #{} must return 1; got {} (rd_hex={}). \
If 0, either _deploy never ran (call #0 should always \
trigger it) OR a subsequent call wiped the slot — \
deploy_triggered gating in execution.rs:274-334 \
regressed.",
i, got, hex::encode(&r.return_data));
}
}
/// **e. Modifier on constructor — inlined require passes through.**
///
/// `modifier nonZero(uint256 x) { require(x > 0, "zero"); _; }`
/// applied to a constructor must inline the same way as on any other
/// function: the modifier body runs, then the underscore-substituted
/// constructor body runs. With a non-zero `_seed` the require passes
/// and the constructor's `seed = _seed` write lands; we read it back
/// via the auto-getter.
///
/// Failure mode: if the modifier was dropped from the constructor
/// (modifier-on-ctor inlining bug), the require never ran but the
/// ctor body still ran, which on a non-zero seed produces the same
/// observable as a passing modifier — so this test does NOT
/// distinguish drop-modifier from inline-modifier on the success
/// path. What it DOES catch: a modifier whose `_;` placeholder was
/// elided (ctor body never ran) — `seed()` would return 0 instead
/// of the fuzzed value.
#[test]
fn constructor_with_modifier(seed in 1u64..=1_000_000u64) {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
uint256 public seed;
modifier nonZero(uint256 x) {
require(x > 0, "zero");
_;
}
constructor(uint256 _seed) nonZero(_seed) {
seed = _seed;
}
}"#;
let arts = compile_contracts(src, false, 2)
.unwrap_or_else(|e| panic!("ctor-lifecycle (e) compile: {:?}", e));
prop_assert!(!arts.is_empty(), "ctor-lifecycle (e) produced no artifacts");
let art = &arts[0];
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("rt");
let r = rt.call_method_with_deploy_args(
&art.bytecode, &art.tokens, &art.manifest,
"seed", &[] as &[StackItem],
Some(&[StackItem::UnsignedInteger(seed)]),
).expect("seed() with modifier-guarded deploy_args host-level (e)");
prop_assert!(r.success,
"seed() post-modifier-on-ctor deploy must succeed for \
non-zero seed ({}); exc={:?}. If exc carries 'zero', the \
modifier's require fired even though _seed > 0 — modifier \
argument-binding regressed for ctor-applied modifiers. If \
exc is some other shape, the modifier was inlined into the \
ctor incorrectly (e.g., the `_;` placeholder dropped, or \
the modifier body was emitted in the wrong scope).",
seed, r.exception.as_ref().map(|e| &e.message));
let got = decode_uint_le(&r.return_data);
prop_assert_eq!(got.clone(), BigUint::from(seed),
"seed() must equal the fuzzed _seed ({}); got {} (rd_hex={}). \
A 0 readback on a non-zero seed means the modifier's `_;` \
was elided from the ctor — the constructor body never ran.",
seed, got, hex::encode(&r.return_data));
}
}