BlockchainSmart ContractsSolidity16 min readUpdated

address Array in Solidity: Beginner Guide With Code

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: address Array in Solidity: Beginner Guide With Code

Section 01 · Definition

What is an address array in Solidity?

An address array is a list of wallets or contract addresses stored on the blockchain. Each entry is the same 20 byte value type, each entry sits at a numbered position, and the list can grow as your contract receives new entries.

Quick answer

What is an address array? An address array is an ordered list where every entry is an Ethereum address. The type is written address[]. Each entry occupies one storage slot. You add an address with push, look one up by index, and count items with length. The default empty value is address(0), which by convention means no one is set.

The keyword address is Solidity's built in type for a 20 byte Ethereum identifier. It can refer to a user wallet or to another contract. Wrapping the type in square brackets, address[], turns it into a list of those identifiers.

Three indexed cells holding shortened Ethereum addresses with a fourth dashed cell labelled push wallet, plus index numbers running from zero to three.
An address array stores wallets in numbered slots. Each entry is exactly 20 bytes, no matter how the address looks when shortened in a UI.

The mental model carries over from the earlier posts in this series. A row of boxes, each holding one value of the same type, numbered from zero. The new rule for beginners is that the empty default is address(0), the zero address. It shows up whenever you read a slot that has not been set yet.

Section 02 · Real use case

A simple real world example

A token launch contract that keeps a small whitelist of wallets allowed to mint during the early sale. The owner adds approved addresses one by one and the contract checks the list when a buyer tries to mint.

The contract needs three operations. Add a wallet to the list. Read a wallet at any position. Know how many wallets are on the list. An address array does all three with a tiny amount of code.

Other places this same shape shows up: a voter roll for a DAO, a contestant roster in a raffle contract, an early holder snapshot before an airdrop, or a participant list for a paid event. Any time you need an ordered set of wallets, an address array is the natural structure.

Section 03 · Smart contract

A complete address array smart contract

A small Whitelist contract with one storage array, one ownership variable, and four functions. Short enough to read in two minutes, complete enough to deploy in Remix.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Whitelist {
    // 1. Track who deployed the contract.
    address public immutable owner;

    // 2. The storage array of approved wallets.
    address[] public allowed;

    constructor() {
        owner = msg.sender;
    }

    // 3. Add a wallet to the whitelist. Owner only.
    function addAddress(address user) external {
        require(msg.sender == owner, "not owner");
        require(user != address(0), "zero address");
        allowed.push(user);
    }

    // 4. Read a wallet at a specific index.
    function getAddress(uint256 i) external view returns (address) {
        require(i < allowed.length, "index out of range");
        return allowed[i];
    }

    // 5. Return how many wallets are on the list.
    function totalAddresses() external view returns (uint256) {
        return allowed.length;
    }
}

Open Remix at remix.ethereum.org, create a new file, paste the contract, compile with the 0.8.20 compiler, and deploy to the JavaScript VM. The deploy panel shows the connected wallet, which is also the owner. Use the second account in the dropdown to confirm the access guard rejects unauthorised callers.

Section 04 · Line by line

What every important line does

Walk through the contract one block at a time. Most lines mirror the earlier posts in this beginner series, with two new patterns that come from the address type.

solidity
address public immutable owner;
address[] public allowed;

constructor() {
    owner = msg.sender;
}

The variable owner holds the deployer's wallet. The keyword immutable says the value is set once at deploy time and never changes. The constructor reads msg.sender, which is the address that submitted the deployment transaction, and saves it.

solidity
function addAddress(address user) external {
    require(msg.sender == owner, "not owner");
    require(user != address(0), "zero address");
    allowed.push(user);
}

Two require guards run before the push. The first checks that the caller is the owner so random wallets cannot pollute the list. The second blocks the zero address, which is almost always a sign of a mistake. After both pass, allowed.push(user) appends the wallet to the array and grows the length by one.

solidity
function getAddress(uint256 i) external view returns (address) {
    require(i < allowed.length, "index out of range");
    return allowed[i];
}

The return type is address, a value type that fits in one storage slot. Unlike strings, addresses do not need a memory or storage label because they are passed by value, just like a uint or an int.

solidity
function totalAddresses() external view returns (uint256) {
    return allowed.length;
}

The length is reported as a plain unsigned integer regardless of the array element type. This pattern is the same across all four types in this series.

Section 05 · Reading and writing

Add data, get data, count items

The three operations look the same as the earlier types. The figure groups them so you can compare against the uint, int, and string equivalents.

Three labelled cards titled write, read, and length showing the matching Solidity code lines for adding a wallet, reading a wallet, and counting the array.
Reading, writing, and counting an address array. The shape is identical to the other three types, only the cell content changes.

Adding data uses allowed.push(msg.sender) when the caller is adding themselves, or allowed.push(user) when the owner is adding a specific wallet. Either way the new entry lands at the end of the list.

Reading data uses bracket lookup. The first wallet is allowed[0], the most recent is allowed[allowed.length - 1] when the list is non empty. Indexes are unsigned, so a bounds check is the standard guard.

Counting items uses allowed.length. The result is a uint256 you can compare in a require, use as an index bound for the methods in Section 06, or return for off chain code to read.

Section 06 · Methods

Add the array methods to Whitelist, one function at a time

The Whitelist contract from Section 03 only has push, indexed read, and length. Here are four more methods every beginner should know, added one function at a time so you can paste each into the same contract and try it in Remix as you go. The owner guard from the original contract carries through.

Start with the Whitelist contract you already have. Each function below goes right after the existing ones inside the contract body. None of them needs a for loop, and each one teaches a distinct rule of Solidity address storage.

Function 1. Remove the last wallet with pop

solidity
function dropLast() external {
    require(msg.sender == owner, "not owner");
    require(allowed.length > 0, "empty");
    allowed.pop();
}

allowed.pop() drops the last wallet and shrinks the length by one. The owner guard keeps the function locked down. The length guard is critical: calling pop on a zero length array triggers a panic and the transaction reverts. Add the length guard whenever the array can be empty at call time.

Function 2. Reset one slot with delete by index

solidity
function clearAt(uint256 i) external {
    require(msg.sender == owner, "not owner");
    require(i < allowed.length, "out of range");
    delete allowed[i];
}

delete allowed[i] writes the type default into the targeted slot. For an address the default is address(0), the zero address. Because most contracts treat the zero address as a special no one is set value, leaving zero address entries inside a live whitelist is rarely what you want. Compact the array with swap and pop in the next section instead.

Function 3. Wipe the entire array with delete on the variable

solidity
function clearAll() external {
    require(msg.sender == owner, "not owner");
    delete allowed;
}

delete allowed on the whole array resets every slot to address(0) and sets the length back to zero in one line. After the call, every indexed read goes out of range, and the next push starts a fresh whitelist from index zero.

Function 4. Replace a wallet with direct assignment

solidity
function replaceAt(uint256 i, address user) external {
    require(msg.sender == owner, "not owner");
    require(i < allowed.length, "out of range");
    require(user != address(0), "zero address");
    allowed[i] = user;
}

Bracket assignment writes a new address into the slot at the given index. The bounds check stops the call early when the index is past the end, and the zero address guard rejects an uninitialised input. The length does not change because the slot already existed.

After adding the four functions, your Whitelist contract now covers all five basic array operations: push, pop, delete by index, delete on the variable, and bracket assignment. Compile in Remix, redeploy, and try each new function from the deploy panel using two accounts to verify the owner guard.

pop and delete are not the same thing

pop actually removes the last wallet and changes the length. delete allowed[i] only resets that one slot to address(0) and leaves the length unchanged. delete allowed resets every slot to address(0) and sets the length back to zero. Pick pop when you want to shrink, pick delete when you want to reset.

Section 07 · Removal

Function 5. Remove any wallet with swap and pop

When the owner needs to drop a wallet from the middle of the whitelist and the enrolment order does not matter, the swap and pop pattern removes the wallet in constant time. Two writes and a pop, no iteration.

Move the last wallet into the slot you want to drop, then call pop. The targeted entry disappears, the last entry takes its place, and the length shrinks by one. The trade off is that the enrolment order changes.

solidity
function removeAt(uint256 i) external {
    require(msg.sender == owner, "not owner");

    uint256 last = allowed.length - 1;
    require(i <= last, "out of range");

    if (i != last) {
        allowed[i] = allowed[last];
    }
    allowed.pop();
}

The if (i != last) guard skips a tiny self assignment when the caller asks for the last slot. After the function returns, the array length is one less than before and the previous last wallet sits at the position that was removed. The cost is constant regardless of how many wallets are on the list.

Section 08 · More patterns

Return the whole array, build memory address arrays, declare fixed length

Three more patterns added one at a time. The first goes onto Whitelist as Function 6. The other two are reference patterns you will reuse in other contracts.

Function 6. Return the whole array as one value

Returning the whole array works when the list is small. The return type uses the memory location keyword because the caller receives a fresh copy of the wallets, not a pointer into storage. Add this function next to the rest in Whitelist.

solidity
function allAllowed() external view returns (address[] memory) {
    return allowed;
}

Pattern 1. Build a temporary memory array

Memory arrays let you build a temporary wallet list inside a function without touching storage. The size is fixed at the moment you create them with new, so you cannot push or pop on a memory array. Set the entries by index after creation. This pattern lives on its own; it does not extend Whitelist because the function is pure.

solidity
function buildBatch(address a, address b) external pure
    returns (address[] memory)
{
    address[] memory batch = new address[](2);
    batch[0] = a;
    batch[1] = b;
    return batch;
}

Pattern 2. Declare a fixed length storage array

Fixed length storage address arrays declare the slot count inside the brackets. You cannot push or pop because the size never changes. Every slot starts at address(0) and you write into the slots by index. This is a separate state variable on its own contract, not an extension of the dynamic Whitelist array.

solidity
// Three slot signer array baked into storage.
address[3] public signers;

function setSigner(uint256 i, address user) external {
    require(i < 3, "out of range");
    require(user != address(0), "zero address");
    signers[i] = user;
}

You cannot resize a dynamic storage array by writing to length

In Solidity 0.5 you could shrink an array with allowed.length = newLen. That assignment was removed in 0.6 and the modern compiler rejects it. Grow with push. Shrink with pop or delete. Reset fully with delete allowed. The length property is read only in every current Solidity version.

Section 09 · Final contract

The complete Whitelist with all six methods together

The original Whitelist from Section 03 plus the six methods you have added function by function. One paste ready file you can drop into Remix and try every operation in a single session, with the owner guard threaded through every mutator.

The contract below is the full version. The owner is captured once in the constructor and every function that mutates the list checks against it. The first three functions are the originals: push wrapped as addAddress, indexed read wrapped as getAddress, and the length read wrapped as totalAddresses. The remaining six are the additions you walked through in Sections 06 to 08.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Whitelist {
    address public immutable owner;
    address[] public allowed;

    constructor() {
        owner = msg.sender;
    }

    // Add a wallet to the whitelist. Owner only.
    function addAddress(address user) external {
        require(msg.sender == owner, "not owner");
        require(user != address(0), "zero address");
        allowed.push(user);
    }

    // Read a wallet at a specific index.
    function getAddress(uint256 i) external view returns (address) {
        require(i < allowed.length, "out of range");
        return allowed[i];
    }

    // Return how many wallets are on the list.
    function totalAddresses() external view returns (uint256) {
        return allowed.length;
    }

    // Function 1. Remove the most recent wallet with pop.
    function dropLast() external {
        require(msg.sender == owner, "not owner");
        require(allowed.length > 0, "empty");
        allowed.pop();
    }

    // Function 2. Reset one slot to address(0) with delete by index.
    function clearAt(uint256 i) external {
        require(msg.sender == owner, "not owner");
        require(i < allowed.length, "out of range");
        delete allowed[i];
    }

    // Function 3. Wipe the whole whitelist with delete on the variable.
    function clearAll() external {
        require(msg.sender == owner, "not owner");
        delete allowed;
    }

    // Function 4. Replace an existing wallet with direct assignment.
    function replaceAt(uint256 i, address user) external {
        require(msg.sender == owner, "not owner");
        require(i < allowed.length, "out of range");
        require(user != address(0), "zero address");
        allowed[i] = user;
    }

    // Function 5. Remove any wallet with swap and pop.
    function removeAt(uint256 i) external {
        require(msg.sender == owner, "not owner");
        uint256 last = allowed.length - 1;
        require(i <= last, "out of range");
        if (i != last) {
            allowed[i] = allowed[last];
        }
        allowed.pop();
    }

    // Function 6. Return the whole array as memory.
    function allAllowed() external view returns (address[] memory) {
        return allowed;
    }
}

Open Remix, paste the contract, compile with the 0.8.20 compiler, and redeploy to the JavaScript VM. The deploy panel now lists nine callable buttons. Use the first account in the dropdown to add three wallets, switch to a second account and confirm the owner guard rejects you on every mutator, then switch back and run removeAt(1) to watch the list compact with swap and pop. Every method you have learnt in this post lives in this single file.

Section 10 · Beginner mistakes

Five mistakes new Solidity developers make with address arrays

Forgetting the zero address check

address(0) is the default value of an empty slot and a sign that something is uninitialised. Always reject it explicitly when accepting an address as input. The standard guard is require(user != address(0), "zero address").

Looping over an open list

If anyone can call addAddress, the list can grow without bound. A function that loops through every entry will eventually run out of gas. For owner only writes the loop is fine. For open enrolment switch to a mapping for membership checks.

Storing duplicates by accident

An address array does not check for uniqueness. Calling push twice with the same wallet stores it twice. If you need a set, pair the array with a mapping(address => bool) and only push when the mapping says the wallet is new.

Confusing payable address with plain address

address can hold any 20 byte value. address payable can also receive Ether. To send Ether to a stored entry you cast it explicitly with payable(allowed[i]). Skipping the cast gives a clear compile error.

Returning huge address arrays from view functions

A view that returns the entire allowed array fails on production wallets when the list grows past a few hundred entries. Paginate the return by accepting a start index and count, or expose individual entries through the public getter.

Section 11 · Practice task

Try this on your own

A small extension that locks in push, pop, delete, swap and pop, and bracket assignment on an address array. Try to ship it without copying from the contract above.

Build a Raffle contract

Add an address[] entrants array and a mapping(address => bool) entered for duplicate detection in constant time. enter() pushes msg.sender, rejects the zero address, and checks entered[msg.sender] before pushing. withdrawEntry() lets a caller leave by reading their index from a separate mapping(address => uint256) and running swap and pop. clearEntrants() resets both data structures with delete. drawWinner() returns one entrant chosen by hashing block.timestamp, block.prevrandao, and entrants.length, modulo the length. The randomness is not safe for real money, but it is fine for a beginner exercise.

Deploy the contract in Remix, switch between three accounts in the deploy panel, and call enter() from each. Confirm the length is three. Call withdrawEntry from one account and confirm the length drops to two. Then call drawWinner() a few times across different blocks and notice the index moves around with the block data.

Section 13 · FAQ

Frequently asked questions

What is an address array in Solidity in simple words?

An address array is an ordered list where every entry is a 20 byte Ethereum address. Each entry sits at a numbered position called an index, starting at zero. The type is address[]. You add a wallet with push, read one by index, and count items with the length property.

How do I add a wallet to a Solidity address array?

Call .push on the array with the address as the argument. For example allowed.push(msg.sender) appends the caller's wallet, and allowed.push(user) appends a specific address. Always guard against the zero address before pushing, because that value usually signals an error or an uninitialised input.

What is the zero address in Solidity?

The zero address is address(0), which is shorthand for 0x0000000000000000000000000000000000000000. It is the default value of any uninitialised address variable and the default value of an empty slot in an address array. By convention it means no wallet is set, so most contracts reject it explicitly.

How do I check if a wallet is in an address array?

Loop through the array and return true on a match. The pattern is simple and works for small arrays. For larger lists where membership is the only question you care about, store the same data in a mapping(address => bool) and check that in constant time. The two together give you ordered enumeration plus quick lookup.

What is the difference between address and address payable?

address holds any 20 byte Ethereum identifier. address payable holds the same value but lets you call .transfer or .send to move Ether to it. To send funds to an entry of an address array, cast it with payable(allowed[i]). For storing wallets in a list and reading them back, plain address is enough.

Can I store contract addresses and wallet addresses in the same Solidity array?

Yes. Solidity does not distinguish wallet addresses from contract addresses at the type level. address[] holds either kind. If you need to be sure an entry is a contract you can call extcodesize on it. For most beginner contracts the distinction does not matter and you can mix the two freely.

How do I remove the last wallet from an address array?

Call .pop on the array, for example allowed.pop(). The function removes the last wallet and shrinks the length by one. pop reverts the transaction when the array is empty, so guard the call with require(allowed.length > 0, ...) whenever the array can be empty at call time.

What does delete do on an address array slot?

delete on a single slot like delete allowed[2] resets that slot to address(0) and leaves the length unchanged. delete on the whole array like delete allowed resets every slot to address(0) and sets the length to zero in one line. delete never frees the storage itself; it just writes the default value in place.

How do I remove a wallet from the middle of an address array without a loop?

Use the swap and pop pattern. Copy the last wallet into the slot you want to remove with allowed[i] = allowed[allowed.length - 1], then call allowed.pop(). The targeted wallet disappears in constant time. The enrolment order of the remaining wallets is no longer the order they joined the list.

Can I shrink a Solidity address array by setting its length?

No. Solidity 0.5 allowed allowed.length = newLen, but the assignment was removed in 0.6 and the modern compiler rejects it. Grow the array with push, shrink it with pop or delete, and reset it fully with delete allowed. The length property is read only in every current Solidity version.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View agentic AI consulting serviceSee ChainTrust case study

Related service

Agentic AI Consulting

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →