# neo-devpack-solidity v0.22.0 + devpack v2.2.0 — Runtime-Fidelity & Audit-Fix Release
**Release date:** 2026-06-17
**Compiler / CLI / workspace:** **v0.22.0**
**devpack (`@neo-devpack-solidity/contracts`):** **v2.2.0**
**Target Neo N3 node:** **v3.10.0** (Gorgon-prep; no hardfork activation)
> Canonical change list: [`CHANGELOG.md`](./CHANGELOG.md) `[v0.22.0]`.
> Previous releases: see the
> [GitHub releases page](https://github.com/r3e-network/neo-devpack-solidity/releases)
> and `CHANGELOG.md` history (previous: v0.21.0 / devpack v2.1.0).
---
## TL;DR
- **22 audit findings, all fixed.** A full systemic audit identified 7
critical runtime-simulator divergences from a real Neo N3 node plus 15
compiler/IR/devpack/CI issues. Every fix was TDD'd and the workspace
stays green (1881 tests, 0 failures).
- **The "passes in simulator, fails on-chain" gap is closed.** `StdLib.
serialize` emits the real Neo binary format; storage gas is mainnet-
aligned (100_000/byte); `CheckSig` verifies real signatures; `CallFlags`
is enforced; storage reverts on inner-call faults; `CreateMultisigAccount`
matches on-chain derivation.
- **Neo-Express on-chain smoke is now a CI gate** — the only oracle that
compares compiler output against a real Neo N3 VM now runs on every PR
(subset) and nightly (full 28-script suite).
- **Target aligned to Neo N3 v3.10.0.** No spec change (v3.10.0 activates
no hardfork), but all version metadata, NuGet deps, TFM, and CI dotnet
versions are brought up to date.
## What changed (high level)
### Runtime simulator (critical)
`StdLib.serialize`/`deserialize` → Neo N3 BinarySerializer format · `CheckSig`
injectable signing hash · `CreateMultisigAccount` via verification-script
construction · storage gas 100_000/byte · `CallFlags` enforcement on storage
writes · storage snapshot/rollback on inner-call revert · `GetNotifications`
returns real data · revert/fault discrimination by payload marker.
### Compiler / IR / devpack
`mulmod`/`addmod` uint256 software divmod · `receive()`+`onNEP17Payment`
coexistence → hard error · NEP-11 self-escrow short-circuit · NEP-24 tokenId
`bytes32`→`bytes` · bytecode emission hardened (3 unchecked writes → Result)
· unsound `x==true→x` optimizer rewrite removed · onNEP17Payment detection
case-insensitive · ETHER_UNIT_RE comment/identifier-aware · inheritance
type-conflict → hard error · `MethodToken::serialize` → Result.
### Toolchain & CI
Target Neo N3 v3.10.0 (neo_version, NuGet 3.10.0, TFM net10.0, CI dotnet
10.0.x) · Neo-Express smoke CI gate (per-PR subset + nightly full).
## Migration notes
- **`receive()` + `onNEP17Payment` together now fails to compile (E105).**
If a contract defined both, the `receive()` body was already dead code —
remove it and consolidate logic into `onNEP17Payment`. A lone `receive()`
still compiles (remapped to a synthetic `onNEP17Payment`).
- **Default `gas_limit` raised 10M → 1B.** Tests that asserted a tight gas
budget should keep constructing their own `RuntimeConfig`; tests that
relied on the old 10M default now have realistic headroom under the
mainnet-aligned storage rate.
- **NEP-24 `royaltyInfo` signature changed** (`bytes32` → `bytes` tokenId).
Update any caller passing a fixed 32-byte id.
## Verification
- `cargo test --workspace`: **1881 passed, 0 failed**.
- `cargo fmt --check` + `cargo clippy -D warnings`: clean.
- `dotnet restore` + `build` (Neo.Sol.Runtime + tests) against Neo 3.10.0:
0 errors.
- devpack standards (NEP-17/11/24/26/27) + examples compile end-to-end.
---
# neo-devpack-solidity v0.21.0 + devpack v2.1.0 — Deep Correctness & Conformance Release
**Release date:** 2026-06-16
**Compiler / CLI / workspace:** **v0.21.0**
**devpack (`@neo-devpack-solidity/contracts`):** **v2.1.0**
> Canonical change list: [`CHANGELOG.md`](./CHANGELOG.md) `[v0.21.0]`.
> Previous releases: see the
> [GitHub releases page](https://github.com/r3e-network/neo-devpack-solidity/releases)
> and `CHANGELOG.md` history (previous: v0.20.0 / devpack v2.0.0).
---
## TL;DR
- **`uint256` arithmetic is now conformant for values `>= 2^255`, end to
end.** Software 256-bit limb routines implement add/sub/mul, checked
overflow, unsigned compare, divmod, and logical/wrapping shifts entirely
in `<= 32`-byte NeoVM operations, so a value like `type(uint256).max` no
longer forms a rejected 33-byte integer and no longer reinterprets as
negative. `type(uint256).max < 5 == false`, `type(uint256).max >> 1`
gives `2^255-1`, unchecked `**`/`+=`/`++` wrap mod `2^256`, and checked
ops Panic(0x11) — matching a real node, with `int256` left on native
signed operations. The runtime simulator was made faithful to a 32-byte
two's-complement NeoVM so it can actually validate this class of behavior.
- **Two full adversarial passes landed.** A 25-defect production audit (24
fixed) and a 12-subsystem systemic best-practice review (33 findings: 32
fixed, 1 surfaced as a loud compile-time warning). Every defect was
reproduced against the compiler/runtime/devpack before fixing and is
covered by regression tests.
- **The ABI codec now covers the two shapes that previously fell back to
Neo-native bytes.** Nested-dynamic types (`string[]`, `bytes[]`, `T[][]`)
and dynamic structs (any struct with a dynamic field) encode and decode
with the full recursive EVM head/tail layout, so cross-contract calldata
and return-data are byte-exact with EVM/solc. `abi.decode` now follows the
encoded head offset instead of assuming `0x20`, accepts over-length
buffers, and faults Panic `0x41` on non-canonical (high-bit-set)
length/offset slots.
- **Selectors and manifests are canonical.** Selectors, `.selector`, and
event `topic0` are computed from EVM-ABI-canonical types (struct → tuple,
enum → `uint8`, `uint` → `uint256`); standard-JSON `methodIdentifiers`,
`methodMap`, and the `abi` array now emit decodable `tuple`/`components`
shapes; `ErrorName.selector` hashes the parametrized signature; fixed-size
arrays (`uint256[3]`) and `bytes20` carry their true ABI type; struct
overloads colliding to the same tuple shape now error.
- **Runtime VM fidelity improved across stack, item, and storage limits.**
Global and per-collection `MaxStackSize` (2048), `MaxItemSize` (65535) on
`NEWBUFFER`/`CAT`, and Neo N3 `Storage.Put` key/value size limits now
fault exactly where a real node would; minimal `CONVERT` Integer→bytes
encoding, corrected `PACKMAP` pop order, `MODPOW(-1)` modular inverse, and
a `SUBSTR` overflow guard close further simulator/real-node gaps.
- **Dispatch is correct and fails loud.** Same-arity overloads resolve by
argument type instead of insertion order; unresolved member calls,
return-tuple arity mismatches, and `>255`-local functions are now hard
compile errors instead of silent mis-dispatch / zero-returns / truncated
slot indices; function-pointer locals dispatch via `CALLA` (and revert
cleanly when zero-init).
- **Devpack fund-safety hardened.** NEP-17 self-escrow no longer faults the
escrow-in leg, zero-amount transfers conform to NEP-17, an
owner-authorized signer quorum gates the escrowed pool, the oracle
conditional-transfer callback no longer strands funds on a short result or
a mis-ordered signature, `multiSigBurn` requires the holder's witness, and
NEP-26 `onNEP11Payment` takes dynamic `bytes` token IDs.
- **Tooling is more honest.** Standard-JSON parse failures emit one
structured `ParseError` per diagnostic with byte ranges, aliased imports
warn (`IMPORT_ALIAS_BY_NAME`), the disassembler decodes the `CALLT`,
`CONVERT`, and `ISTYPE` operands, and the compiler warns when a literal
exceeds NeoVM's 32-byte integer limit.
## Verification
- `cargo test`: full unit, e2e, conformance, differential, and
property/fuzz suites green — including new property tests pinning the
software 256-bit add/sub/mul/divmod/shift/compare routines against a
reference 256-bit model across the `[2^255, 2^256-1]` range, and ABI
round-trip tests for nested-dynamic and dynamic-struct encode/decode.
- `cargo clippy --all-targets`: clean. `cargo fmt --check`: clean.
- neo-express on-chain smoke tests remain the real-chain ground truth
(deploy, constructor args, CALLT, permissions, low-level/external calls,
structs, upgrade lifecycle, witness guards, oracle relay); the native NEP
Transfer / iterator / Basilisk-notification validations from v0.20.0
continue to pass.
- Every fixed defect from both review passes ships with a regression test;
the two surfaced-but-not-silenced items (the `uint256` literal
representation warning and the `IMPORT_ALIAS_BY_NAME` warning) are
asserted as warnings rather than hard failures.
## Breaking changes
- **devpack NEP-26 receiver shape.** `INEP26Receiver.onNEP11Payment`
tokenId is now a dynamic `bytes` (was `bytes32`), matching
`INEP11Receiver` and the ByteString token IDs the NEP-11 base passes. A
contract implementing the old `bytes32` signature must update its
parameter type to be invoked correctly; a non-32-byte id no longer
mis-encodes.
- **Standard-JSON error shape.** Parse failures now emit one
`type:"ParseError"` entry per diagnostic, each with
`sourceLocation:{file,start,end}` byte offsets, instead of a single
`type:"Generic"` blob. Tooling that pattern-matched the old single
`Generic` error must read the per-diagnostic `ParseError` array.
- **New hard compile errors.** Unresolved member calls (`inner.member(args)`
matching no resolution branch), functions exceeding NeoVM's 255 local-slot
limit, explicit return-tuple arity mismatches, same-tuple-shape struct
overloads, and out-of-range `bytesN` (`bytes0`, `bytes33`+) now fail the
build. These previously compiled silently (member calls returned `0` and
dropped arguments; `>255` locals truncated slot indices; mismatches were
warnings), producing NEFs that mis-dispatched or contradicted their own
ABI. Code relying on those silent behaviors must be corrected.
- **`bytes20` and fixed-size-array selectors changed.** `bytes20` now
canonicalizes to `bytes20` (was `address`) and `uint256[3]`-style
parameters now canonicalize to `T[N]` (was `T[]`), so the keccak selector,
`interfaceId`, and `abi.encodeWithSelector` payload for any function taking
these types change to the solc/ethers-correct values. Off-chain callers
hard-coding the old (incorrect) selectors must recompute them.
- **`msg.value` now lowers to `0`.** Neo N3 has no EVM-style attached call
value; the prior `System.Runtime.GetMsgValue` syscall FAULTed on a real
node. Contracts that read `msg.value` must instead take received amounts
from the `amount` argument of `onNEP17Payment`/`onNEP11Payment`.
- **Aliased imports warn.** `import {A as B}` and `import * as NS` now emit
an `IMPORT_ALIAS_BY_NAME` warning (resolution and codegen unchanged), since
aliases resolve by the underlying symbol name and a name collision in the
import closure could bind to the wrong declaration. Builds treating
warnings as errors must allowlist this code.
## Known limitations
- `uint256` software arithmetic is scoped to genuinely-typed `uint256`;
signed `int256` deliberately keeps native NeoVM operations (`-x-1` NOT,
arithmetic shift, signed divmod), so the two integer widths follow
different code paths by design.
- The bundled emulator, now made faithful to a 32-byte two's-complement
NeoVM with the stack/item/storage-limit checks above, remains a
development tool rather than consensus-grade: gas is approximated and
exception-unwinding parity is partial (see
`docs/NEO_VM_PARITY_TODO.md`). neo-express smoke tests are the real-chain
ground truth.
- Modular-arithmetic sign conventions follow NeoVM's C#-style truncated
remainder (`MODMUL`/`MODPOW`, `%`), which differs from the Euclidean
non-negative form some EVM tooling assumes for negative operands; this is
intentional NeoVM parity, not an EVM match.
- The minimal `CONVERT` Integer→ByteString/Buffer encoding is scoped to the
Solidity-observable CONVERT path; the internal storage/map-key byte helper
keeps its own fixed encoding, so the two are not interchangeable at the
byte level.