Within an Ethereum transaction, the zero-account is just a special case used to indicate that a new contract is being deployed. It is literally '0x0' set to the to field in the raw transaction.
Every Ethereum transaction, whether it's a transfer between two external accounts, a request to execute contract code, or a request to deploy a new contract, are encoded in the same way. A raw transaction object will look something like this:
transaction = {
nonce: '0x0',
gasLimit: '0x6acfc0', // 7000000
gasPrice: '0x4a817c800', // 20000000000
to: '0x0',
value: '0x0',
data: '0xfffff'
};
If to is set to something other than '0x0', this request will result in transferring ether to the address (if value is non-zero), and execute the function encoded in the data field. Remember, the address can either be a contract or an external account.
When the to address is the zero-address, a new contract will be created by executing the code in data (this is what is meant by "code that returns the code"). The address of the newly created contract is technically known beforehand as it's based on the address of the sender and it's current nonce. That address becomes the official address of the contract after mining.
For a pretty good read on Ethereum transactions, check out this blog post.
Note: There is also the actual Solidity code statement address(0) which is the initial value of a variable of type address. The documentation you posted, however, is referring to specifically when the to account address in a transaction is set to '0x0'.
Within an Ethereum transaction, the zero-account is just a special case used to indicate that a new contract is being deployed. It is literally '0x0' set to the to field in the raw transaction.
Every Ethereum transaction, whether it's a transfer between two external accounts, a request to execute contract code, or a request to deploy a new contract, are encoded in the same way. A raw transaction object will look something like this:
transaction = {
nonce: '0x0',
gasLimit: '0x6acfc0', // 7000000
gasPrice: '0x4a817c800', // 20000000000
to: '0x0',
value: '0x0',
data: '0xfffff'
};
If to is set to something other than '0x0', this request will result in transferring ether to the address (if value is non-zero), and execute the function encoded in the data field. Remember, the address can either be a contract or an external account.
When the to address is the zero-address, a new contract will be created by executing the code in data (this is what is meant by "code that returns the code"). The address of the newly created contract is technically known beforehand as it's based on the address of the sender and it's current nonce. That address becomes the official address of the contract after mining.
For a pretty good read on Ethereum transactions, check out this blog post.
Note: There is also the actual Solidity code statement address(0) which is the initial value of a variable of type address. The documentation you posted, however, is referring to specifically when the to account address in a transaction is set to '0x0'.
It's not actually true that a contract creation transaction has a "to" field set to the zero address (meaning 0x00...000). This is an easy mistake to make (and I've made it too) as it is described that way in many resources.
The passage you cite from the Solidity docs were updated to state this:
If the target account is not set (the transaction does not have a recipient or the recipient is set to null), the transaction creates a new contract. As already mentioned, the address of that contract is not the zero address but an address derived from the sender and its number of transactions sent (the “nonce”).
So you can see they realized at some point that the recipient field should be empty. I've actually looked at serialized creation transactions and found 0x80 there instead of an RLP-ed zero address.
In fact, 0x80 is the RLP encoding of an empty byte array, which is what the Yellow Paper states is the recipient for a contract creation:
The address hash
is slightly different: it is either a 20-byte address hash or, in the case of being a contract-creation transaction (and thus formally equal to ∅), it is the RLP empty byte sequence and thus the member of
As I said, this is a common source of confusion. In that vein, this GitHub PR rolling back a mistakenly "fixed" test is amusing. It has the comment:
RLP encoding of 0 is encoding of empty byte array, so 0x80 is correct.
0x00 is encoding of byte array of length 1 containing one byte 0, not encoding of integer 0.
You can simply use 0, or 0x0000000000000000000000000000000000000000.
address(0) in Solidity is equivalent to 0x0000000000000000000000000000000000000000.
You must put single quotes around it. '0x0000000000000000000000000000000000000000' worked for me Been hunting down that bug for some time ...
So, for now, transferring to what we call a long-zero address (an address obtained by converting an accountId to its solidity expression 0.0.536603 -> 0x0000000000000000000000000000000000536603) via a JSON-RPC transaction will fail.
However, transferring to an EVM address using a JSON-RPC transaction will work, see example below:
console.log("Transfer to EVM address");
let tx = {
to: `0xb8482dc06049cdff56a6b747330f06de3cc1efc0`,
value: ethers.parseEther('3', 'wei')
};
let transaction = await signer.sendTransaction(tx);
The EVM address for an account only exists if the account was first created by sending hbar or tokens to this address. An account created with the SDK will only have a long-zero EVM address.
Alternatively, you can use the SDK to transfer to any of the three address types:
import {
AccountId,
Client, Hbar,
PrivateKey, TransferTransaction
} from "@hashgraph/sdk";
import dotenv from "dotenv";
dotenv.config();
async function main() {
let client = Client.forName(process.env.HEDERA_NETWORK).setOperator(
AccountId.fromString(process.env.OPERATOR_ID),
PrivateKey.fromString(process.env.OPERATOR_KEY)
);
console.log("Transfer to accountId");
response = await new TransferTransaction()
.addHbarTransfer(client.operatorAccountId, new Hbar(1).negated())
.addHbarTransfer(AccountId.fromString("0.0.5465603"), new Hbar(1))
.execute(client);
await response.getReceipt(client);
console.log("Transfer to solidity address");
response = await new TransferTransaction()
.addHbarTransfer(client.operatorAccountId, new Hbar(1).negated())
.addHbarTransfer("0x0000000000000000000000000000000000536603", new Hbar(1))
.execute(client);
await response.getReceipt(client);
console.log("Transfer to EVM address");
let response = await new TransferTransaction()
.addHbarTransfer(client.operatorAccountId, new Hbar(1).negated())
.addHbarTransfer("0xb8482dc06049cdff56a6b747330f06de3cc1efc0", new Hbar(1))
.execute(client);
await response.getReceipt(client);
client.close();
}
void main();
It turns out to be a little more nuanced than the above, you can indeed send to a long-zero address, however...
If the account has an EVM address, you have to use the EVM address.
For example: account 0.0.5465603 was created by sending hbar to 0xb8482dc06049cdff56a6b747330f06de3cc1efc0, as a result, only this address can be used with ethers.js.
If the account was created via the SDK and doesn't have an EVM address, but a long-zero address, you can use ethers.js to send to the long zero address.
For example: account 0.0.220237 was created with the SDK and has a long zero address: 0x0000000000000000000000000000000000219b07 which can be used by ethers.js
Also tried through a contract
pragma solidity ^0.8.26;
contract SendTest {
function send(address payable to) public payable {
to.transfer(msg.value);
}
}
deployed here on testnet: 0x000000000000000000000000000000000053da1a
Sending to the long zero address of an account (0.0.536603) created by sending hbar to an evm address fails
const testContract = new ethers.Contract(`0x${contractAddress}`, abi, signer)
const options = {
value: 20000000000, // 2 tinybar
gasLimit: 100000
}
await testContract.send("0x0000000000000000000000000000000000536603", options);
Sending to the EVM address of that account (0.0.536603) works
const testContract = new ethers.Contract(`0x${contractAddress}`, abi, signer)
const options = {
value: 20000000000, // 2 tinybar
gasLimit: 100000
}
await testContract.send("0xb8482dc06049cdff56a6b747330f06de3cc1efc0", options);
Sending to the long zero address of an account created with the SDK (0.0.2202375) works
const testContract = new ethers.Contract(`0x${contractAddress}`, abi, signer)
const options = {
value: 20000000000, // 2 tinybar
gasLimit: 100000
}
await testContract.send("0x0000000000000000000000000000000000219b07", options);
Section 4.2 : The Transaction from the Ethereum Yellow Paper (p4-5) defines the "zero-address / zero-account" as an:
RLP empty byte sequence.
So it Ethereum or Solidity, it looks like:
0x0000000000000000000000000000000000000000 // "zero-address" = 20 bytes of '00'
As described below, there is a conditional check to see if the transaction intend to transfer funds or create a new contract:
- If it's a 20-byte address hash, then it is a transfer of ethers.
- If it's an RLP empty byte sequence, then it is a contract creation.

The "zero account" is really just the fake account associated with the address 0x00000...
Sending funds there doesn't actually transfer them to the account; instead, the miners interpret this as an instruction to create a new smart contract.
You can use ethers constants.
In hardhat tests, ethers lib are imported by default, so you don't need to add the require sentence.
Happy coding!
it('Should not allow 0 address to be added', async () => {
await expect(greeter.addWhitelistAddress(ethers.constants.AddressZero)).to.be.revertedWith(
'Invalid address'
);
});
You can use openzeppelin's test library for this.
it("Should return an error when address 0 is entered", async function () {
const {
constants,
expectRevert,
} = require('@openzeppelin/test-helpers');
const AWLContract = await ethers.getContractFactory("AddWhiteList");
const addWhiteList = await AWLContract.deploy();
console.log(constants.ZERO_ADDRESS);
await addWhiteList.deployed();
await expectRevert(
addWhiteList.addWhitelistAddress(constants.ZERO_ADDRESS),
'Invalid address',
);
});
So you should change the quotes in the messages with "Invalid address".