
Chain Industries
January 09, 2026
Preparing for Solidity 0.9.0: What’s Deprecated and What to Do About It



Solidity 0.9.0 is coming, and it’s going to break things.
If you’ve compiled your contracts with Solidity 0.8.31 or later, you’ve probably noticed new deprecation warnings. These aren’t suggestions, they’re advance notice that features you might be using will stop working entirely when 0.9.0 drops.
This is the first breaking release since 0.8.0 landed in December 2020. That’s four years of accumulated changes, and the Solidity team is using this release to clean house.
The good news: you have time to prepare. The deprecation warnings are live now, the breaking changes are documented, and migration is straightforward if you start early.
This article covers what’s being removed, why it matters, and exactly how to update your code.
1. transfer() and send() Are Dead
This is the big one. If you’re still using .transfer() or .send() to move ETH, your code will not compile in Solidity 0.9.0.
Why They’re Being Removed
Both transfer() and send() were originally designed as "safe" ways to send ETH because they forward only 2,300 gas to the recipient. The idea was that this gas limit would prevent reentrancy attacks, the recipient wouldn't have enough gas to call back into your contract.
The problem: gas costs change.
When EIP-1884 increased the cost of SLOAD from 200 to 800 gas in the Istanbul upgrade, contracts that previously worked fine with 2,300 gas suddenly started failing. A simple receive function that logged an event or updated a state variable would run out of gas.
The 2,300 gas stipend is now considered a footgun, not a safety feature. It breaks legitimate contracts and doesn’t reliably prevent reentrancy anyway (there are other attack vectors).
What the Deprecation Looks Like
If you compile this with Solidity 0.8.31+:
function withdraw() external { payable(msg.sender).transfer(address(this).balance); }
You'll see:
Warning: Using "transfer" is deprecated. Use "call" instead, and be aware of
re-entrancy vulnerabilities.
How to Fix It
Replace transfer() and send() with call():
Before:
function withdraw() external { payable(msg.sender).transfer(amount); }
After
function withdraw() external { (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); }
But What About Reentrancy?
Yes, call() forwards all available gas, which means the recipient can execute arbitrary code — including calling back into your contract.
You need to handle this explicitly. Two options:
Option 1: Checks-Effects-Interactions Pattern
Update state before making external calls:
function withdraw() external { uint256 amount = balances[msg.sender]; // Effect: Update state first balances[msg.sender] = 0; // Interaction: External call last (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); }
Option 2: Reentrancy Guard
Use OpenZeppelin’s ReentrancyGuard or implement your own:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract Vault is ReentrancyGuard { function withdraw() external nonReentrant { uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); } }
Option 3: Transient Storage Reentrancy Guard (Gas Efficient)
If you’re targeting post-Dencun chains, use transient storage for the lock:
contract Vault { bytes32 constant LOCK_SLOT = keccak256("reentrancy.lock"); modifier nonReentrant() { assembly { if tload(LOCK_SLOT) { revert(0, 0) } tstore(LOCK_SLOT, 1) } _; assembly { tstore(LOCK_SLOT, 0) } } function withdraw() external nonReentrant { uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); } }
This saves ~5,000 gas per call compared to storage-based guards.
2. Virtual Modifiers Are Going Away
In Solidity 0.9.0, you will no longer be able to mark modifiers as virtual or override them in derived contracts.
What’s Changing
Currently, this is valid:
contract Base { modifier onlyOwner() virtual { require(msg.sender == owner, "Not owner"); _; } } contract Derived is Base { modifier onlyOwner() override { require(msg.sender == owner || msg.sender == admin, "Not authorized"); _; } }
In 0.9.0, this will not compile.
Why It’s Being Removed
Virtual modifiers create confusing inheritance behavior. When a function uses a modifier, and that modifier is overridden in a derived contract, it’s not always clear which version of the modifier executes, especially in complex inheritance hierarchies.
The Solidity team decided that the added complexity isn’t worth the flexibility. Functions can still be overridden, which covers most legitimate use cases.
How to Fix It
Option 1: Use a Virtual Function Inside the Modifier
contract Base { modifier onlyAuthorized() { require(_isAuthorized(msg.sender), "Not authorized"); _; } function _isAuthorized(address account) internal virtual returns (bool) { return account == owner; } } contract Derived is Base { function _isAuthorized(address account) internal override returns (bool) { return account == owner || account == admin; } }
The modifier stays fixed, but the authorization logic it calls can be customized.
Option 2: Accept the Fixed Modifier
In many cases, you don’t actually need to override modifiers. If your derived contracts need different access control, consider using a different modifier entirely:
contract Base { modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } } contract Derived is Base { modifier onlyAdmin() { require(msg.sender == admin, "Not admin"); _; } function adminFunction() external onlyAdmin { // ... } }
3. ABI Coder v1 Is Finally Gone
ABI coder v1 has been deprecated since Solidity 0.8.0, where v2 became the default. In 0.9.0, v1 is being removed entirely.
What’s Different in v2
ABI coder v2 supports:
Nested dynamic arrays (uint256[][])
Structs in function parameters and return values
More consistent encoding/decoding behavior
If you’re explicitly using v1:
pragma abicoder v1;
This will not compile in 0.9.0.
How to fix it
Step 1: Remove the pragma
Delete any pragma abicoder v1; declarations.
Step 2: Check for Encoding Differences
In most cases, the switch is transparent. However, if you’re doing low-level ABI encoding/decoding or interacting with contracts that expect v1 encoding, you may need to verify compatibility.
Key differences to watch:
v2 validates dynamic types more strictly on decoding
v2 handles nested arrays differently
Some edge cases around padding and encoding order
Step 3: Test Your External Integrations
If your contract calls other contracts or is called by off-chain systems that manually encode calldata, verify that the encoding matches expectations.
// Test that external calls still work function testExternalCall() external { bytes memory data = abi.encodeWithSignature( "someFunction(uint256[][])", nestedArray ); (bool success, ) = targetContract.call(data); require(success, "Call failed"); }
4. memory-safe-assembly Comment Syntax Removed
If you’re using inline assembly with the memory-safe annotation via a NatSpec comment, you need to switch to the proper syntax.
What’s Changing
Old (deprecated):
/// @solidity memory-safe-assembly assembly { // ... }
New (required):
assembly ("memory-safe") { // ...}
Why It Matters
The memory-safe annotation tells the optimizer that your assembly block follows Solidity's memory model. This enables certain optimizations that would otherwise be disabled globally when any assembly is present.
The comment-based syntax was a temporary workaround for backwards compatibility. Now that the proper syntax has been available for several versions, the workaround is being removed.
How to Fix It
Find and replace all instances:
# Find all files with the old syntaxgrep -r "@solidity memory-safe-assembly" contracts
Then update each one:
Before:
function efficientHash(bytes memory data) internal pure returns (bytes32 result) { /// @solidity memory-safe-assembly assembly { result := keccak256(add(data, 32), mload(data)) }}
After:
function efficientHash(bytes memory data) internal pure returns (bytes32 result) { assembly ("memory-safe") { result := keccak256(add(data, 32), mload(data)) }}
5. Contract Type Comparisons Deprecated
Comparing variables of different contract types is now deprecated and will be removed in 0.9.0.
What’s Changing
This will not compile in 0.9.0:
contract A {}contract B {}function compare(A a, B b) external pure returns (bool) { return a == b; // Deprecated: comparing different contract types}
Why It’s Being Removed
Comparing two different contract types is almost always a bug. If A and B are different contracts, comparing them directly doesn't make semantic sense, you're comparing addresses, but the type system suggests you're comparing contract instances.
The Solidity team decided to make this a compile error to catch bugs early.
How to Fix It
If you actually need to compare the underlying addresses, cast explicitly:
Before:
function isSameAddress(A a, B b) external pure returns (bool) { return a == b;}
After:
function isSameAddress(A a, B b) external pure returns (bool) { return address(a) == address(b);}
This makes the intent clear: you’re comparing addresses, not contract instances.
6. How to Check Your Codebase
Don’t wait for 0.9.0 to find out what breaks. Use 0.8.31+ now to surface all deprecation warnings.
Step 1: Update Your Compiler
In your foundry.toml:
[profile.default]solc_version = "0.8.33"
Or in hardhat.config.js:
module.exports = { solidity: "0.8.33",};
Step 2: Compile and Capture Warnings
# Foundryforge build 2>&1 | grep -i "deprecated"# Hardhatnpx hardhat compile 2>&1 | grep -i "deprecated"
Step 3: Address Each Warning
Step 4: Run Your Test Suite
After each change, verify nothing broke:
forge test
Step 5: Consider a Phased Rollout
If you’re maintaining deployed contracts:
Fix warnings in development: update your codebase now
Deploy new versions: new deployments use updated code
Migrate when ready: existing deployments can migrate via upgrades (if upgradeable) or redeployment
Conclusion
Solidity 0.9.0 is cleaning up years of accumulated technical debt. The deprecations make sense, they remove footguns, simplify the language, and encourage better patterns.
The key is to start now:
Upgrade to 0.8.31+ to see all deprecation warnings
Fix the high-priority items (transfer/send) first
Test thoroughly after each change
Don’t wait for 0.9.0 to break your CI pipeline
If you’re maintaining a large codebase or want a second opinion on your migration strategy, we’re happy to help.
Solidity 0.9.0 is coming, and it’s going to break things.
If you’ve compiled your contracts with Solidity 0.8.31 or later, you’ve probably noticed new deprecation warnings. These aren’t suggestions, they’re advance notice that features you might be using will stop working entirely when 0.9.0 drops.
This is the first breaking release since 0.8.0 landed in December 2020. That’s four years of accumulated changes, and the Solidity team is using this release to clean house.
The good news: you have time to prepare. The deprecation warnings are live now, the breaking changes are documented, and migration is straightforward if you start early.
This article covers what’s being removed, why it matters, and exactly how to update your code.
1. transfer() and send() Are Dead
This is the big one. If you’re still using .transfer() or .send() to move ETH, your code will not compile in Solidity 0.9.0.
Why They’re Being Removed
Both transfer() and send() were originally designed as "safe" ways to send ETH because they forward only 2,300 gas to the recipient. The idea was that this gas limit would prevent reentrancy attacks, the recipient wouldn't have enough gas to call back into your contract.
The problem: gas costs change.
When EIP-1884 increased the cost of SLOAD from 200 to 800 gas in the Istanbul upgrade, contracts that previously worked fine with 2,300 gas suddenly started failing. A simple receive function that logged an event or updated a state variable would run out of gas.
The 2,300 gas stipend is now considered a footgun, not a safety feature. It breaks legitimate contracts and doesn’t reliably prevent reentrancy anyway (there are other attack vectors).
What the Deprecation Looks Like
If you compile this with Solidity 0.8.31+:
function withdraw() external { payable(msg.sender).transfer(address(this).balance); }
You'll see:
Warning: Using "transfer" is deprecated. Use "call" instead, and be aware of
re-entrancy vulnerabilities.
How to Fix It
Replace transfer() and send() with call():
Before:
function withdraw() external { payable(msg.sender).transfer(amount); }
After
function withdraw() external { (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); }
But What About Reentrancy?
Yes, call() forwards all available gas, which means the recipient can execute arbitrary code — including calling back into your contract.
You need to handle this explicitly. Two options:
Option 1: Checks-Effects-Interactions Pattern
Update state before making external calls:
function withdraw() external { uint256 amount = balances[msg.sender]; // Effect: Update state first balances[msg.sender] = 0; // Interaction: External call last (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); }
Option 2: Reentrancy Guard
Use OpenZeppelin’s ReentrancyGuard or implement your own:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract Vault is ReentrancyGuard { function withdraw() external nonReentrant { uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); } }
Option 3: Transient Storage Reentrancy Guard (Gas Efficient)
If you’re targeting post-Dencun chains, use transient storage for the lock:
contract Vault { bytes32 constant LOCK_SLOT = keccak256("reentrancy.lock"); modifier nonReentrant() { assembly { if tload(LOCK_SLOT) { revert(0, 0) } tstore(LOCK_SLOT, 1) } _; assembly { tstore(LOCK_SLOT, 0) } } function withdraw() external nonReentrant { uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); } }
This saves ~5,000 gas per call compared to storage-based guards.
2. Virtual Modifiers Are Going Away
In Solidity 0.9.0, you will no longer be able to mark modifiers as virtual or override them in derived contracts.
What’s Changing
Currently, this is valid:
contract Base { modifier onlyOwner() virtual { require(msg.sender == owner, "Not owner"); _; } } contract Derived is Base { modifier onlyOwner() override { require(msg.sender == owner || msg.sender == admin, "Not authorized"); _; } }
In 0.9.0, this will not compile.
Why It’s Being Removed
Virtual modifiers create confusing inheritance behavior. When a function uses a modifier, and that modifier is overridden in a derived contract, it’s not always clear which version of the modifier executes, especially in complex inheritance hierarchies.
The Solidity team decided that the added complexity isn’t worth the flexibility. Functions can still be overridden, which covers most legitimate use cases.
How to Fix It
Option 1: Use a Virtual Function Inside the Modifier
contract Base { modifier onlyAuthorized() { require(_isAuthorized(msg.sender), "Not authorized"); _; } function _isAuthorized(address account) internal virtual returns (bool) { return account == owner; } } contract Derived is Base { function _isAuthorized(address account) internal override returns (bool) { return account == owner || account == admin; } }
The modifier stays fixed, but the authorization logic it calls can be customized.
Option 2: Accept the Fixed Modifier
In many cases, you don’t actually need to override modifiers. If your derived contracts need different access control, consider using a different modifier entirely:
contract Base { modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } } contract Derived is Base { modifier onlyAdmin() { require(msg.sender == admin, "Not admin"); _; } function adminFunction() external onlyAdmin { // ... } }
3. ABI Coder v1 Is Finally Gone
ABI coder v1 has been deprecated since Solidity 0.8.0, where v2 became the default. In 0.9.0, v1 is being removed entirely.
What’s Different in v2
ABI coder v2 supports:
Nested dynamic arrays (uint256[][])
Structs in function parameters and return values
More consistent encoding/decoding behavior
If you’re explicitly using v1:
pragma abicoder v1;
This will not compile in 0.9.0.
How to fix it
Step 1: Remove the pragma
Delete any pragma abicoder v1; declarations.
Step 2: Check for Encoding Differences
In most cases, the switch is transparent. However, if you’re doing low-level ABI encoding/decoding or interacting with contracts that expect v1 encoding, you may need to verify compatibility.
Key differences to watch:
v2 validates dynamic types more strictly on decoding
v2 handles nested arrays differently
Some edge cases around padding and encoding order
Step 3: Test Your External Integrations
If your contract calls other contracts or is called by off-chain systems that manually encode calldata, verify that the encoding matches expectations.
// Test that external calls still work function testExternalCall() external { bytes memory data = abi.encodeWithSignature( "someFunction(uint256[][])", nestedArray ); (bool success, ) = targetContract.call(data); require(success, "Call failed"); }
4. memory-safe-assembly Comment Syntax Removed
If you’re using inline assembly with the memory-safe annotation via a NatSpec comment, you need to switch to the proper syntax.
What’s Changing
Old (deprecated):
/// @solidity memory-safe-assembly assembly { // ... }
New (required):
assembly ("memory-safe") { // ...}
Why It Matters
The memory-safe annotation tells the optimizer that your assembly block follows Solidity's memory model. This enables certain optimizations that would otherwise be disabled globally when any assembly is present.
The comment-based syntax was a temporary workaround for backwards compatibility. Now that the proper syntax has been available for several versions, the workaround is being removed.
How to Fix It
Find and replace all instances:
# Find all files with the old syntaxgrep -r "@solidity memory-safe-assembly" contracts
Then update each one:
Before:
function efficientHash(bytes memory data) internal pure returns (bytes32 result) { /// @solidity memory-safe-assembly assembly { result := keccak256(add(data, 32), mload(data)) }}
After:
function efficientHash(bytes memory data) internal pure returns (bytes32 result) { assembly ("memory-safe") { result := keccak256(add(data, 32), mload(data)) }}
5. Contract Type Comparisons Deprecated
Comparing variables of different contract types is now deprecated and will be removed in 0.9.0.
What’s Changing
This will not compile in 0.9.0:
contract A {}contract B {}function compare(A a, B b) external pure returns (bool) { return a == b; // Deprecated: comparing different contract types}
Why It’s Being Removed
Comparing two different contract types is almost always a bug. If A and B are different contracts, comparing them directly doesn't make semantic sense, you're comparing addresses, but the type system suggests you're comparing contract instances.
The Solidity team decided to make this a compile error to catch bugs early.
How to Fix It
If you actually need to compare the underlying addresses, cast explicitly:
Before:
function isSameAddress(A a, B b) external pure returns (bool) { return a == b;}
After:
function isSameAddress(A a, B b) external pure returns (bool) { return address(a) == address(b);}
This makes the intent clear: you’re comparing addresses, not contract instances.
6. How to Check Your Codebase
Don’t wait for 0.9.0 to find out what breaks. Use 0.8.31+ now to surface all deprecation warnings.
Step 1: Update Your Compiler
In your foundry.toml:
[profile.default]solc_version = "0.8.33"
Or in hardhat.config.js:
module.exports = { solidity: "0.8.33",};
Step 2: Compile and Capture Warnings
# Foundryforge build 2>&1 | grep -i "deprecated"# Hardhatnpx hardhat compile 2>&1 | grep -i "deprecated"
Step 3: Address Each Warning
Step 4: Run Your Test Suite
After each change, verify nothing broke:
forge test
Step 5: Consider a Phased Rollout
If you’re maintaining deployed contracts:
Fix warnings in development: update your codebase now
Deploy new versions: new deployments use updated code
Migrate when ready: existing deployments can migrate via upgrades (if upgradeable) or redeployment
Conclusion
Solidity 0.9.0 is cleaning up years of accumulated technical debt. The deprecations make sense, they remove footguns, simplify the language, and encourage better patterns.
The key is to start now:
Upgrade to 0.8.31+ to see all deprecation warnings
Fix the high-priority items (transfer/send) first
Test thoroughly after each change
Don’t wait for 0.9.0 to break your CI pipeline
If you’re maintaining a large codebase or want a second opinion on your migration strategy, we’re happy to help.
More articles

Automating Trust and Efficiency
The Role of Smart Contracts in Web3 Revolution
January 25, 2025

A New Standard for Asset Protection
Securing Digital Assets in the Web3 Era
February 10, 2025

Balancing Innovation and Compliance
Navigating the Regulatory Landscape of Web3