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: UNLICENSEDpragmasolidity^0.8.20;import"hardhat/console.sol";contract CounterError{uint256public number;uint256publicconstant MAX_NUMBER =100;addresspublic owner;constructor() { owner =msg.sender; console.log("Contract deployed by:", owner); console.log("Initial number:", number); console.log("MAX_NUMBER:", MAX_NUMBER);}functionsetNumber(uint256newNumber)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);}functionincrement()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);}functiongetState()publicviewreturns(uint256currentNumber,uint256maxNumber,addresscurrentOwner){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
You can write a test file named CounterCreat.test.js to help detect the issue and implement a solution.
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:
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:
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:
Which will then throw an error:
You can include some debugging logs to identify the issue:
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.
function createCounter(uint256 _initialValue) external payable {
Counter newCounter = new Counter{ value: msg.value }(_initialValue);
counters.push(newCounter);
}
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];
}
}
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);
}
});
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]);
}
}
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]);
}
}