# Solidity Since 0.8.16: Four Years of Changes in One Post

Author: Markus Waas

Published: 2026-10-23T12:00:00.000Z

Source: [https://soliditydeveloper.com/solidity-since-0-8-16](<https://soliditydeveloper.com/solidity-since-0-8-16>)

![A release train runs along a track from 2022 to 2026. Its carriages carry Solidity version numbers and it stops at six stations named after Ethereum upgrades.](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/3ec9e8a0a1837888/f7892342c867/v/0949152e3e6d/release-train-cover.webp>)

Back in 2020 we [looked at Solidity 0.8.0](<https://soliditydeveloper.com/solidity-0.8>) and I joked that after 0.9 might come 0.10. Six years later we are at 0.8.38, released on October 22, 2026, and 0.9 is still a rumor. The joke aged well. Better than `selfdestruct`, anyway.

The last four years alone brought 23 minor releases, from 0.8.16 in August 2022 to 0.8.38. None of them was a breaking one, which is exactly why they are easy to miss. A new mapping syntax here, a new keyword there, one opcode per hardfork. Add it all up and the Solidity you write today looks quite different from the Solidity of mid 2022. So let's catch up, release by release where it matters and grouped by topic where it doesn't.

If you only want the raw list, the [official changelog](<https://github.com/ethereum/solidity/blob/develop/Changelog.md>) has everything. This post is the version with opinions.

## The timeline at a glance

| Version | Date | Why you remember it |
| --- | --- | --- |
| 0.8.16 | Aug 2022 | Calldata tuple re-encoding bugfix |
| 0.8.17 | Sep 2022 | Storage write removal bugfix |
| 0.8.18 | Feb 2023 | Paris by default, `block.prevrandao`, `selfdestruct` warning, named mapping parameters |
| 0.8.19 | Feb 2023 | User-defined operators |
| 0.8.20 | May 2023 | Shanghai by default, `PUSH0` |
| 0.8.21 | Jul 2023 | Relaxed immutables, events from other contracts |
| 0.8.22 | Oct 2023 | Free loop increments, file-level events |
| 0.8.23 | Nov 2023 | `verbatim` deduplication bugfix |
| 0.8.24 | Jan 2024 | Cancun opcodes: `tstore`, `tload`, `mcopy`, blobs |
| 0.8.25 | Mar 2024 | Cancun by default, `MCOPY` in generated code |
| 0.8.26 | May 2024 | `require` with custom errors (via IR), faster optimizer |
| 0.8.27 | Sep 2024 | `require` with custom errors everywhere, IR caching |
| 0.8.28 | Oct 2024 | `transient` state variables |
| 0.8.29 | Mar 2025 | Custom storage layout with `layout at`, experimental EOF |
| 0.8.30 | May 2025 | Prague by default |
| 0.8.31 | Dec 2025 | Osaka by default, `clz`, a wave of deprecations |
| 0.8.32 / 0.8.33 | Dec 2025 | Storage array bugfix, plus a hotfix the same day |
| 0.8.34 | Feb 2026 | Transient storage clearing bugfix |
| 0.8.35 | Apr 2026 | `erc7201` builtin, `--experimental` flag, SSA CFG code generator |
| 0.8.36 | Jul 2026 | EOF backend removed, Amsterdam support |
| 0.8.37 | Sep 2026 | `block.slotnum`, more deprecations, three miscompilation fixes |
| 0.8.38 | Oct 2026 | Yul optimizer bugfix, faster compilation |

Yes, that is a lot of table. Let's turn it into something useful.

## 1. The compiler follows the hardforks

The most predictable pattern of the last four years: Ethereum ships an upgrade, and shortly before or after, Solidity adds the new opcodes and switches its default EVM version. Six times in a row.

![Timeline with two rows. The upper row shows Ethereum upgrades from Paris in September 2022 to Fusaka in December 2025. The lower row shows the Solidity release that made each upgrade's EVM version the default, usually within a few weeks.](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/5d19a5eaec46808b/afc2ea10b297/v/3c403e5ca95f/default-evm-timeline.webp>)

### Paris (0.8.18)

The Merge replaced `block.difficulty` with the RANDAO value, and Solidity 0.8.18 gave that value a proper name: `block.prevrandao`. The old `block.difficulty` is deprecated and `difficulty()` is no longer allowed in inline assembly when targeting Paris or later. I wrote a whole [deep dive on PREVRANDAO](<https://soliditydeveloper.com/prevrandao>) if you want to know why you should be careful with it.

The same release started warning about `selfdestruct`, following [EIP-6049](<https://eips.ethereum.org/EIPS/eip-6049>). There is still no replacement, and Cancun later changed its semantics (more on that below). If you have it in a contract you plan to deploy, remove it.

### Shanghai (0.8.20)

Shanghai brought `PUSH0`, a one-byte way to put a zero on the stack. Solidity 0.8.20 uses it automatically and made Shanghai the default EVM version. Cheaper deployment, cheaper runtime, zero work for you.

Zero work, that is, unless you deploy to a chain that didn't support `PUSH0` yet. Quite a few L2s took a while, and for months the most common support question was 'why does my deployment fail with invalid opcode'. If you still target an older chain, set `--evm-version` explicitly. The compiler cannot know where your bytecode is going.

### Cancun (0.8.24 and 0.8.25)

Cancun was the big one for Solidity developers. Version 0.8.24 added the new opcodes as Yul builtins:

- `tstore()` and `tload()` for transient storage ([EIP-1153](<https://eips.ethereum.org/EIPS/eip-1153>))
- `mcopy()` for copying memory ([EIP-5656](<https://eips.ethereum.org/EIPS/eip-5656>))
- `blobhash()` and `blobbasefee()`, plus `blobhash(uint)` and `block.blobbasefee` in Solidity itself ([EIP-4844](<https://eips.ethereum.org/EIPS/eip-4844>), [EIP-7516](<https://eips.ethereum.org/EIPS/eip-7516>))

Cancun also changed `selfdestruct` ([EIP-6780](<https://eips.ethereum.org/EIPS/eip-6780>)). It now only transfers the Ether balance. Code and storage stay, unless the contract is destroyed in the same transaction that created it. This happens at the network level, so your compiler version doesn't matter here. The warning from 0.8.18 does, though.

One day after Dencun went live on mainnet, 0.8.25 made Cancun the default and started using `MCOPY` for memory copies in generated code. The savings are modest, since copies from calldata, storage or returndata still work the old way.

### Prague (0.8.27 and 0.8.30)

Prague support arrived in 0.8.27 and became the default in 0.8.30, on the day Pectra activated. There were no new opcodes for Solidity to wrap this time. The interesting part was [EIP-7702](<https://eips.ethereum.org/EIPS/eip-7702>), which lets an externally owned account run contract code. That turned storage layout into a hot topic, and it is the reason custom storage layouts were prioritized for 0.8.29. We'll get there.

Pectra also made calldata more expensive for data-heavy transactions ([EIP-7623](<https://eips.ethereum.org/EIPS/eip-7623>)). The compiler does not try to account for this; the team suggests tuning `--optimize-runs` instead.

### Osaka (0.8.29 and 0.8.31)

Osaka support was added in 0.8.29 and became the default in 0.8.31, on the same day Fusaka activated. The one new opcode for us is `CLZ`, count leading zeros ([EIP-7939](<https://eips.ethereum.org/EIPS/eip-7939>)). It is available as the Yul builtin `clz(x)` and is mostly interesting for math libraries. Finding the highest set bit used to take a small binary search in assembly. Now it's one instruction.

```solidity
function log2Floor(uint256 x) pure returns (uint256 result) {
    require(x != 0);
    // requires EVM version osaka or later
    assembly {
        result := sub(255, clz(x))
    }
}
```

### Amsterdam (0.8.36 and 0.8.37)

Amsterdam is not live yet, but the compiler already supports it as a target. Solidity 0.8.37 added `block.slotnum`, the beacon chain slot number of the current block ([EIP-7843](<https://eips.ethereum.org/EIPS/eip-7843>)). If you ever tried to approximate epochs from block numbers, like we did in the PREVRANDAO post, you will appreciate it. And 0.8.38 already warns if you use `swapn`, `dupn` or `exchange` as Yul identifiers, because they will become reserved names.

## 2. Language features you will actually use

Hardforks aside, the language itself grew. Here are the additions I would reach for in a new project today, in the order you are likely to meet them.

### Named mapping parameters (0.8.18)

Small change, big readability win. You can finally name the key and the value of a mapping:

```solidity
mapping(address owner => mapping(address spender => uint256 amount)) public allowance;
```

The names are purely documentation and change nothing about the generated code. Compare it to `mapping(address => mapping(address => uint256))` and tell me which one you would rather audit at 2am.

### User-defined operators (0.8.19)

User-defined value types exist since 0.8.8, but they were awkward to use because you had to `wrap` and `unwrap` everything. Since 0.8.19 you can bind operators to pure free functions:

```solidity
pragma solidity ^0.8.19;

type UFixed18 is uint256;

using {add as +, mul as *, eq as ==} for UFixed18 global;

function add(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
    return UFixed18.wrap(UFixed18.unwrap(a) + UFixed18.unwrap(b));
}

function mul(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
    return UFixed18.wrap(UFixed18.unwrap(a) * UFixed18.unwrap(b) / 1e18);
}

function eq(UFixed18 a, UFixed18 b) pure returns (bool) {
    return UFixed18.unwrap(a) == UFixed18.unwrap(b);
}

contract Pricing {
    function total(UFixed18 price, UFixed18 quantity) external pure returns (UFixed18) {
        return price * quantity; // calls mul(price, quantity)
    }
}
```

You can define the arithmetic operators `+ - * / %`, the bitwise ones `& | ^ ~` and all comparisons. The binding has to be `global` in the file that defines the type, the functions must be pure free functions, and all parameters have to be the type itself. No `+=`, no `&&`, no `**`, and no redefining operators of built-in types. Which is fine. Nobody needs `uint256 + uint256` to mean something surprising.

### Immutables without ceremony (0.8.21)

Before 0.8.21, immutables had to be assigned exactly once, and not inside an `if`, a loop or a `try`. Now you can read and write them anywhere during construction, as often as you like, as long as it happens outside of functions and modifiers. An immutable you never assign is simply zero.

```solidity
contract FeeVault {
    uint256 public immutable fee;
    address public immutable guardian; // never assigned, so address(0)

    constructor(uint256 fee_) {
        fee = fee_;
        if (fee_ > 100) {
            fee = 100; // overwriting during construction is fine since 0.8.21
        }
    }
}
```

### Events outside of contracts (0.8.21 and 0.8.22)

Two related changes. Since 0.8.21 you can emit an event that is defined in a contract, interface or library you don't inherit from, using a qualified name. Since 0.8.22 you can define events at file level, right next to your free functions and custom errors. The era of the `library Events {}` wrapper is over.

```solidity
event Transfer(address indexed from, address indexed to, uint256 value);

interface IVault {
    event Deposited(address indexed who, uint256 amount);
}

contract Token {
    function transfer(address to, uint256 value) external {
        emit Transfer(msg.sender, to, value);     // file-level event, since 0.8.22
        emit IVault.Deposited(msg.sender, value); // qualified access, since 0.8.21
    }
}
```

Both are also included in the contract's ABI now, which 0.8.20 fixed for events defined outside the emitting contract.

### Free loop increments (0.8.22)

Remember the gas golf pattern where you wrote the loop without an increment and did `unchecked { ++i; }` at the end of the body? You can stop. Since 0.8.22 the compiler drops the overflow check itself when it can prove the counter cannot overflow.

![Two-panel comic. Left panel titled Gas Golfer: a proud developer presents a for loop with unchecked plus plus i. Right panel titled Solidity 0.8.22: a friendly compiler robot holds the same loop without the unchecked block and says it does that automatically now.](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/f95f9214c33e396b/c0a923366a38/v/def209666465/unchecked-loop-comic.webp>)

```solidity
// 2022: the gas golf version
for (uint256 i = 0; i < items.length;) {
    total += items[i];
    unchecked { ++i; }
}

// since 0.8.22: same bytecode, readable
for (uint256 i = 0; i < items.length; ++i) {
    total += items[i];
}
```

The conditions are strict: the condition must be a `<` comparison with the counter on the left, the right side must fit the counter's type without widening, the counter must be a local built-in integer, the loop expression must be `i++` or `++i`, and the body must not touch the counter. `<=` doesn't qualify. The optimization runs even with the optimizer disabled, and you can switch it off in Standard JSON with `simpleCounterForLoopUncheckedIncrement: false` if you really want to.

### require with custom errors (0.8.26 and 0.8.27)

Custom errors exist since 0.8.4, but using them meant writing `if (!condition) revert MyError();` every time. Since 0.8.26 `require` has a third overload that takes an error. It was IR-only at first, and 0.8.27 brought it to the legacy pipeline as well.

```solidity
error InsufficientBalance(uint256 available, uint256 required);

contract Token {
    mapping(address account => uint256 balance) public balance;

    function transfer(address to, uint256 amount) external {
        require(
            balance[msg.sender] >= amount,
            InsufficientBalance(balance[msg.sender], amount)
        );
        balance[msg.sender] -= amount;
        balance[to] += amount;
    }
}
```

One thing to keep in mind: the error arguments are evaluated whether the condition holds or not, like any function argument. Don't put anything with side effects in there. And if you use named arguments in that position, use 0.8.37 or later; earlier IR builds encoded them in the wrong order, which only affected the revert data but still makes for a confusing debugging session.

### Transient storage (0.8.24 to 0.8.28)

This one took three releases. The `tstore` and `tload` builtins came with Cancun in 0.8.24, 0.8.27 taught the parser the `transient` keyword, and 0.8.28 finally generated code for it. Transient storage behaves like storage within one transaction and is reset to zero when the transaction ends. Reads and writes cost 100 gas each. The textbook use case is a reentrancy lock:

```solidity
pragma solidity ^0.8.28;

contract Guarded {
    bool transient locked;

    modifier nonReentrant() {
        require(!locked, "Reentrancy attempt");
        locked = true;
        _;
        locked = false;
    }

    function withdraw() external nonReentrant {
        // ...
    }
}
```

The current restrictions: only value types, no initializer at the declaration, and `transient` cannot be combined with `constant` or `immutable`. Structs, arrays and mappings are not supported yet. The compiler warns on the first `tstore` in assembly for a reason: a value that survives across calls within a transaction breaks the assumption that batching calls behaves like sending them separately. Reset your transient variables at the end of the call, and don't skip the reset of the lock above to save gas. That would limit your contract to one call per transaction.

Also worth knowing: compiling with `--via-ir` and using `delete` on a transient variable had a nasty bug between 0.8.28 and 0.8.33, where the compiler could emit `sstore` instead of `tstore` or the other way around. Fixed in 0.8.34. The team found only three affected deployed contracts, so you are probably fine, but check your compiler version.

### Custom storage layout (0.8.29, 0.8.31 and 0.8.35)

Here is the feature EIP-7702 made urgent. When an EOA delegates to contract code, every delegate it ever uses shares the same storage. If two of them both put their first variable in slot zero, they will happily overwrite each other. Namespaced storage ([ERC-7201](<https://eips.ethereum.org/EIPS/eip-7201>)) solved this with inline assembly and a lot of boilerplate. Since 0.8.29 the compiler can do it:

```solidity
pragma solidity ^0.8.35;

contract Wallet layout at erc7201("soliditydeveloper.storage.Wallet") {
    address public owner;
    uint256 public nonce;
}
```

The `layout at` specifier moves the base slot of the whole contract, including everything inherited. In 0.8.29 only literal expressions were allowed, 0.8.31 added constants, and 0.8.35 added `erc7201()`, the first builtin that is evaluated at compile time rather than folded by the optimizer. A few rules: the specifier goes on the most derived contract only, not on abstract contracts, interfaces or libraries, and it doesn't affect transient variables. The compiler also refuses layouts that would push your static variables past the end of storage.

## 3. The compiler wants a word

Non-breaking releases cannot remove anything. What they can do is warn, and the Solidity team has been using that lever more and more as 0.9.0 gets closer. If your build log has turned yellow lately, this is why.

![A tiny vintage coin purse labeled 2300 gas sits on a pedestal. A museum placard reads: Gas stipend, 2016 to 2025. Enough for one event back then.](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/dfef4d316ffcb818/50890945f85a/v/d733e4bc2a3a/send-transfer-stipend.webp>)

**`send()` and `transfer()`** (deprecated in 0.8.31). The 2300 gas stipend was enough for a storage read and an event in 2016, and the whole idea depended on opcode prices never changing. They changed. Both functions are now considered an anti-pattern and will be removed in 0.9.0. Use `call` and check the result, together with a reentrancy guard or checks-effects-interactions:

```solidity
error TransferFailed();

function payout(address payable to, uint256 amount) internal {
    // deprecated since 0.8.31, gone in 0.9.0
    // to.transfer(amount);

    (bool ok, ) = to.call{value: amount}("");
    require(ok, TransferFailed());
}
```

**Virtual modifiers** (0.8.31). Overriding a modifier in a derived contract turned out to be surprising for users and painful for the compiler. Calling virtual functions from inside a modifier stays allowed.

**Comparing contracts** (0.8.31). `contractA < contractB` compiles today and nobody is sure what it should mean. From 0.9.0 you will have to convert to `address` first.

**ABI coder v1 and `pragma experimental ABIEncoderV2`** (0.8.31 and 0.8.37). Coder v2 has been the default since 0.8.0 and is the only option with `--via-ir`. The pragma is pure noise now, mostly living on in old forge-std versions.

**The `memory-safe-assembly` comment** (0.8.31). Use `assembly ("memory-safe") { ... }` instead. Same effect, actual syntax.

**Old EVM versions.** Everything before Constantinople was deprecated in 0.8.22, and 0.8.37 added `constantinople`, `petersburg`, `istanbul` and `berlin` to the list. If you still compile for Byzantium, the Solidity team would genuinely like to hear from you.

**Future keywords** (0.8.35). `at`, `error`, `layout`, `leave`, `super`, `this` and `transient` will become keywords in 0.9.0, and a list of Yul builtins such as `tload`, `mcopy` and `prevrandao` will become reserved. If you named a variable `layout`, now is a good time to rename it.

**`block.difficulty` and `selfdestruct`** (0.8.18). Still here, still warning, both headed for the exit.

## 4. The compiler itself

Not every important change is visible in the language. Three storylines ran in the background for the whole four years.

### via-IR got fast

Compiling through Yul used to be the slow, memory-hungry option you enabled when the legacy pipeline said 'stack too deep'. Three releases changed that. Version 0.8.26 replaced the default optimizer sequence with a single pass that cut IR compile times by up to 65 percent. Version 0.8.27 started caching optimized Yul for contracts that other contracts create with `new`, which cut compile times on projects like Uniswap v4 and EigenLayer by more than 40 percent. And 0.8.28 generated Yul ASTs only on demand, cutting memory use by up to 80 percent. If your last experience with `via_ir = true` was a coffee break, try again.

### The long war on stack too deep

![Two-panel comic. Left panel labeled 2022: a young developer stares at a monitor that reads Stack too deep. Right panel labeled 2026: the same developer, now with grey hair and a bigger coffee, stares at the same message, while a sticky note on the monitor reads experimental via SSA CFG.](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/0cae8f74fe1bcdc7/f060a6c30b4f/v/4e41f37f654c/stack-too-deep-comic.webp>)

Every Solidity developer has met this error, and the 2025 developer survey named it the most common recurring issue. The fight went through three phases.

First, 0.8.21 enabled the stack-to-memory mover by default for via-IR, which moves variables that don't fit on the stack into memory. It solved most cases, at the cost of a few extra memory accesses.

Second, hope arrived in the form of EOF, the EVM Object Format. Version 0.8.29 shipped an experimental EOF backend whose `SWAPN` and `DUPN` instructions reach 256 stack slots instead of 16, which would have made the error nearly extinct. Then EOF was dropped from Fusaka late in the process, and 0.8.36 removed the backend again, citing no mainnet support and no realistic prospect of adoption. The consolation prize: `SWAPN`, `DUPN` and `EXCHANGE` are coming anyway, without EOF, as part of the Amsterdam upgrade ([EIP-8024](<https://eips.ethereum.org/EIPS/eip-8024>)). That is why 0.8.38 already reserves their names in Yul.

![A small gravestone in a tidy garden reads: Experimental EOF backend, 0.8.29 to 0.8.36. It almost fixed stack too deep. A developer in a hoodie leaves a single flower.](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/c4e06d1b249b51a0/1c8f994a8d90/v/3936130c9df2/eof-tombstone.webp>)

Third, the current plan: a new code generator based on SSA form control-flow graphs, introduced in 0.8.35 as experimental and improved in every release since. Version 0.8.36 taught it to spill values to memory, so code that fails with stack too deep on both existing pipelines can compile through it. Version 0.8.37 replaced its stack shuffler with a planning one. You can try it with `solc --experimental --via-ssa-cfg`, and making it the default for `--via-ir` is the team's stated priority. Experimental means experimental, though: don't ship mainnet bytecode from it yet.

### Experimental is now a flag

Speaking of which, 0.8.35 introduced a top-level `--experimental` flag (or `settings.experimental: true` in Standard JSON). Nothing experimental works without it, the state is recorded in the metadata, and the team is explicit that these features may change in breaking ways between minor releases. Along the way, several experiments were removed for good: the EWasm backend (0.8.21), the typed Yul dialect (0.8.27), the language server mode and the `pragma experimental solidity` prototype (both 0.8.37). That last one moved to a separate project, which brings us to the future.

## 5. The bugs worth knowing about

Minor releases are also where the security fixes land. Most of them affect code almost nobody writes, but a few are worth remembering if you maintain older deployments.

- **0.8.16**: ABI re-encoding of calldata tuples could zero the first 32 bytes of a dynamic value when the last component was a static array. Affected structs, external function arguments, events and errors.
- **0.8.17**: the Yul optimizer could remove storage writes before an assembly function that conditionally calls `return()` or `stop()`.
- **0.8.21**: `<expression>.selector` didn't always evaluate the expression in the legacy pipeline, dropping side effects.
- **0.8.23**: blocks that only differed inside `verbatim` instructions were deduplicated into one.
- **0.8.32**: clearing or copying arrays that straddle the end of storage could skip writes. Present since 0.1.0, only reachable with unusual layouts. Version 0.8.32 also introduced an internal compiler error on getters of constants, so 0.8.33 followed a few hours later. Skip 0.8.32.
- **0.8.34**: the transient storage clearing bug described above.
- **0.8.36**: two medium ones. Mutually recursive functions could share spilled memory slots in the IR pipeline, and the warning about layouts too close to the end of storage could reverse the inheritance order, changing constructor and initialization order. Version 0.8.37 fixed a related spill slot collision that went back to 0.7.2.
- **0.8.37**: `delete` on one element of a memory `bytes` array zeroed the whole 32-byte word in the legacy pipeline. That one existed in every version up to 0.8.36.
- **0.8.38**: the Yul optimizer could keep stale knowledge about memory and storage addresses after a variable was reassigned, from 0.8.19 to 0.8.37. Comparisons across Sourcify-verified contracts found no changed bytecode, so it is rare in practice.

The pattern here is the usual one: upgrade regularly, and read the [list of known bugs](<https://docs.soliditylang.org/en/latest/bugs.html>) before you pin a version for years.

## 6. What's next: 0.9 and Core Solidity

The deprecation wave tells you a breaking 0.9.0 release is being prepared, but there is no date yet. The bigger news is what the team calls [Core Solidity](<https://www.soliditylang.org/blog/2026/10/14/the-state-of-core-solidity/>): a new frontend for the same compiler with algebraic data types, pattern matching with exhaustiveness checking, generics, traits and compile-time evaluation. It compiles to Yul, so it shares the optimizer and the new SSA code generator with today's language, now called Classic Solidity. The two are meant to coexist in one project.

Here is a taste from the [pattern matching post](<https://www.soliditylang.org/blog/2026/05/05/pattern-matching-in-core-solidity/>). Instead of a struct with an enum and five fields that are only valid in some combinations, a payment is one of three shapes, and the compiler refuses a `match` that forgets one:

```javascript
data Payment =
    Native(address, word)
  | ERC20(address, address, address, word)
  | ERC721(address, address, address, tokenid);

function processPayment(payment : Payment) {
    match payment {
    | Native(to, amount) =>
        transfer(to, amount);
    | ERC20(token, from, to, amount) =>
        transferFromERC20(token, from, to, amount);
    | ERC721(token, from, to, tokenId) =>
        transferFromERC721(token, from, to, tokenId);
    }
}
```

Core Solidity is a prototype. Checked arithmetic, events, custom errors and `view` don't exist yet, inheritance is deliberately gone in favor of traits and modules, and the team says plainly that it is not for mainnet. You can play with it in the [online playground](<https://playground.solcore.soliditylang.org/>), and there is a workshop at Devcon in Mumbai on November 5, 2026. I'm curious. Pattern matching in a smart contract language is overdue.

## Should you upgrade?

Same checklist as in 2020, with better odds this time.

- Are you below 0.8.22? Upgrading gets you free loop optimizations, `PUSH0` and file-level events without touching your code. Check that your target chain supports `PUSH0`, or pin `--evm-version`.
- Are you using `--via-ir`? Go straight to 0.8.37 or later. That skips the transient storage clearing bug, the spill slot collisions and the misordered `require` arguments.
- Are you between 0.8.28 and 0.8.33 and use `delete` on a transient variable? Upgrade today.
- Do your warnings mention `transfer`, `send`, virtual modifiers or `ABIEncoderV2`? Fix them now while they are warnings. They will be errors in 0.9.0.
- Are you deploying to an L2? Double check its EVM version before trusting the compiler default, which is Osaka now.

Four years, 23 releases, no breaking changes, and still a very different language at the end of it. Not bad for a project that was supposed to hit 1.0 eventually.

Happy Solidity coding!
