Identifying Common Errors
When working with smart contracts, especially on the ICB Network, it's important to recognize and understand common errors that may occur during development. Being able to identify and resolve these issues efficiently is a key part of the debugging process.
In our ICB Learn tutorial series, we address several compile-time errors in the Error Triage section. However, other issues—such as transaction reverts or out-of-bounds array access—can surface unexpectedly during contract execution.
In the sections below, you'll explore proven techniques to help you debug and troubleshoot these runtime errors effectively.
Revert Errors
When a transaction fails because of a require or revert statement, it's essential to investigate why the condition wasn't satisfied. This often means checking input parameters, contract state variables, or specific conditions within the logic.
For example, in the Counter.sol
contract, there's a require statement that ensures the newNumber parameter is less than 100. To debug this type of error, a practical approach is to log newNumber:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "hardhat/console.sol";
contract CounterError {
uint256 public number;
uint256 public constant MAX_NUMBER = 100;
address public owner;
constructor() {
owner = msg.sender;
console.log("Contract deployed by:", owner);
console.log("Initial number:", number);
console.log("MAX_NUMBER:", MAX_NUMBER);
}
function setNumber(uint256 newNumber) public {
console.log("setNumber called with:", newNumber);
console.log("Current number:", number);
console.log("Caller:", msg.sender);
console.log("Owner:", owner);
console.log("MAX_NUMBER:", MAX_NUMBER);
require(msg.sender == owner, "Only owner can set the number");
require(newNumber <= MAX_NUMBER, "Number cannot exceed maximum limit");
number = newNumber;
console.log("Number successfully set to:", newNumber);
}
function increment() public {
console.log("increment called");
console.log("Current number:", number);
console.log("Caller:", msg.sender);
require(msg.sender == owner, "Only owner can increment");
require(number < MAX_NUMBER, "Cannot increment: would exceed maximum");
number++;
console.log("Number incremented to:", number);
}
function getState() public view returns (
uint256 currentNumber,
uint256 maxNumber,
address currentOwner
) {
return (number, MAX_NUMBER, owner);
}
}
When you run the tests with npx hardhat test
, you'll then see the following:

You can now clearly see the value of newNumber, which makes it easier to identify the issue. In this case, it's evident that newNumber is less than 100, which explains why the condition fails.
Unintended Behavior Errors
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import "hardhat/console.sol";
import {Counter} from "./Counter.sol";
contract CounterCreator {
Counter[] internal counters;
function createCounter(uint256 _initialNumber) external payable {
Counter newCounter = new Counter();
newCounter.setNumber(_initialNumber);
counters.push(newCounter);
console.log("Ether received but stuck in CounterCreator:", msg.value);
}
function getCountersCount() external view returns (uint256) {
return counters.length;
}
function getCounter(uint256 index) external view returns (address) {
require(index < counters.length, "Counter index out of bounds");
return address(counters[index]);
}
}
You can write a test file named CounterCreat.test.js
to help detect the issue and implement a solution.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe.only("CounterCreator Tests", function () {
const INITIAL_NUMBER = 42;
const VALUE_SENT = ethers.parseEther("0.01");
let counterCreatorInstance;
let ownerSigner;
let userSigner;
before(async () => {
const signers = await ethers.getSigners();
ownerSigner = signers[0];
userSigner = signers[1];
const CounterCreatorFactory = await ethers.getContractFactory("CounterCreator");
counterCreatorInstance = await CounterCreatorFactory.connect(ownerSigner).deploy();
});
it("should demonstrate the stuck Ether problem", async () => {
const initialBalance = await ethers.provider.getBalance(counterCreatorInstance.target);
expect(initialBalance).to.equal(0);
await counterCreatorInstance.connect(userSigner).createCounter(1, {
value: VALUE_SENT,
});
const contractBalance = await ethers.provider.getBalance(counterCreatorInstance.target);
expect(contractBalance).to.equal(VALUE_SENT);
const targetContract = await counterCreatorInstance.getCounter(0);
const targetContractBalance = await ethers.provider.getBalance(targetContract);
console.log("ETH received by target contract", ethers.formatEther(targetContractBalance));
expect(targetContractBalance).to.equal(0);
});
});
The terminal output below shows that the balance is 0:

Although this issue can be caught by writing more comprehensive test cases with proper assertions, the missing transfer of Ether from CounterCreator to the Counter contract is an important detail that might have been unintentionally overlooked.
To resolve this, you should update the createCounter
function as follows:
function createCounter(uint256 _initialValue) external payable {
Counter newCounter = new Counter{ value: msg.value }(_initialValue);
counters.push(newCounter);
}
Out-of-Bounds Errors
Accessing an array with an invalid index can result in runtime errors.
For example, in a CounterCreator
contract, you might use a custom function like this to retrieve all Counter
contract instances:
function getAllCounters() external view returns (Counter[] memory result) {
result = new Counter[](counters.length);
for (uint i = 0; i <= counters.length; i++) {
result[i] = counters[i];
}
}
However, this implementation contains a common mistake: the loop condition i <= counters.length
causes an out-of-bounds access on the last iteration. It should be i < counters.length
instead.
While you could make the counters
array public to access the data directly, using a custom function like getAllCounters
is useful for illustrating and controlling how the data is exposed.
You can verify the function using the following test case:
it("should get all counters", async () => {
await counterCreatorInstance.connect(userSigner).createCounter(10, {
value: VALUE_SENT,
});
const allCounters = await counterCreatorInstance.getAllCounters();
console.log("All counter addresses:", allCounters);
expect(allCounters.length).to.equal(1);
for (let i = 0; i < allCounters.length; i++) {
expect(allCounters[i]).to.not.equal(ethers.ZeroAddress);
console.log(`Counter ${i} address:`, allCounters[i]);
}
for (let i = 0; i < allCounters.length; i++) {
const individualAddress = await counterCreatorInstance.getCounter(i);
expect(allCounters[i]).to.equal(individualAddress);
}
});
Which will then throw an error:

You can include some debugging logs to identify the issue:
function getAllCounters() external view returns (address[] memory result) {
result = new address[](counters.length);
console.log("Counters length %s", counters.length);
for (uint i = 0; i <= counters.length; i++) {
console.log("Counter index %s", i);
result[i] = address(counters[i]);
}
}
Then, you see the following in the terminal:

Since arrays in Solidity are zero-indexed, an array containing one item stores that item at index 0. In the example above, the if statement uses <=
in the loop condition, which causes it to attempt access at index 1—an invalid position—resulting in a crash.
Here's a straightforward fix:
function getAllCounters() external view returns (address[] memory result) {
result = new address[](counters.length);
console.log("Counter length %s", counters.length);
for (uint i = 0; i < counters.length; i++) {
console.log("Counter index %s", i);
result[i] = address(counters[i]);
}
}
Which immediately solves the problem:

Last updated