Skip to main content

Debugging smart contracts

=nil; comes equipped with the Cometa service which is a tool for storing contract metadata and analyzing messages. If a message fails due to a bug in contract logic, Cometa can pinpoint the exact line of Solidity code where the issue that caused the failure has occurred.

Working with the Cometa service involves these steps:

  • Creating a JSON file with the description of the compilation task
  • Compiling the contract and deploying it
  • Registering the contract with the Cometa service
  • Using the =nil; developer tools to investigate any failed messages to the contract
Playground integration

Whenever a contract is deployed via the =nil; Playground, it is automatically registered inside the Cometa service.

Draft an example contract

To illustrate how debugging works, this tutorial uses the following contract:

pragma solidity ^0.8.0;

contract CounterBug {
uint256 private value;

event ValueChanged(uint256 newValue);

function increment() public {
require(msg.sender == address(0));
value += 1;
emit ValueChanged(value);
}

function getValue() public view returns (uint256) {
return value;
}
}

Inside the increment() function, the contract has a require() statement with a condition that will never evaluate to true unless the contract is called from the zero address. This is done to deliberately trigger an ExecutionReverted error.

Create a file with the compilation task

As input, Cometa takes a JSON file storing a compilation task. This task includes the compiler version, settings, and the contract files to be compiled.

See the below example on how this file should be structured:

{
"contractName": "CounterBug.sol:CounterBug",
"compilerVersion": "0.8.28",
"settings": {
"evmVersion": "shanghai",
"optimizer": {
"enabled": false,
"runs": 200
}
},
"sources": {
"CounterBug.sol": {
"urls": ["./CounterBug.sol"]
}

}
}
tip

Make sure that the compiler version in the input file is compatible with the specified EVM target.

Note that the "sources" key must contain all .sol files used during contract compilation including any imported contracts.

For locally imported contracts:

"sources": {
"CounterBug.sol": {
"urls": ["./CounterBug.sol"]
},
"Nil.sol": {
"urls": ["path/to/Nil.sol"]
}
}

For contracts imported from packages:

"sources": {
"CounterBug.sol": {
"urls": ["./CounterBug.sol"]
},
"@nilfoundation/smart-contracts/Nil.sol": {
"urls": ["path/to/Nil.sol"]
}
}

Compile the contract, deploy it, and register it

Via the =nil; CLI

To compile the contract, deploy it and register it inside Cometa:

nil wallet deploy --compile-input path/to/counter.json --salt SALT

Alternatively, compile the contract:

solc -o path/to/CounterBug --bin --abi path/to/CounterBug.sol --overwrite --no-cbor-metadata --metadata-hash none"

Deploy the contract separately:

nil wallet deploy path/to/CounterBug.bin --abi path/to/CounterBug.abi --shard-id 2 --salt SALT

Register the contract with the Cometa service:

nil cometa register --address COUNTER_BUG_ADDRESS_SEPARATE --compile-input path/to/counter.json 

Via Nil.js

To compile the contract, deploy it and register it inside Cometa:


import {
CometaService,
convertEthToWei,
Faucet,
generateRandomPrivateKey,
HttpTransport,
LocalECDSAKeySigner,
PublicClient,
waitTillCompleted,
WalletV1,
} from "@nilfoundation/niljs";
const cometa = new CometaService({
transport: new HttpTransport({
endpoint: COMETA_ENDPOINT,
}),
});

const client = new PublicClient({
transport: new HttpTransport({
endpoint: RPC_ENDPOINT,
}),
shardId: 1,
});

const faucet = new Faucet(client);
const signer = new LocalECDSAKeySigner({
privateKey: generateRandomPrivateKey(),
});

const pubkey = await signer.getPublicKey();
const wallet = new WalletV1({
pubkey: pubkey,
salt: BigInt(Math.floor(Math.random() * 10000)),
shardId: 1,
client,
signer,
});

const walletAddress = wallet.address;
await faucet.withdrawToWithRetry(walletAddress, convertEthToWei(1));
await wallet.selfDeploy(true);

const counterBugJson = `{
"contractName": "CounterBug.sol:CounterBug",
"compilerVersion": "0.8.28",
"settings": {
"evmVersion": "shanghai",
"optimizer": {
"enabled": false,
"runs": 200
}
},
"sources": {
"CounterBug.sol": {
"urls": ["./CounterBug.sol"]
}

}
}`;

const compilationResult = await cometa.compileContract(counterBugJson);

const { address, hash } = await wallet.deployContract({
bytecode: compilationResult.code,
abi: compilationResult.abi as unknown as Abi,
args: [],
salt: BigInt(Math.floor(Math.random() * 10000)),
feeCredit: 500_000n,
shardId: 1,
});

const receipts = await waitTillCompleted(client, hash);

if (receipts.some((receipt) => !receipt.success)) {
throw new Error("Contract deployment failed");
}

await cometa.registerContractData(compilationResult, address);

const incrementHash = await wallet.sendMessage({
to: address,
functionName: "increment",
abi: compilationResult.abi as unknown as Abi,
feeCredit: 300_000n,
});

await waitTillCompleted(client, incrementHash);

Via the Hardhat plugin

Set the Cometa endpoint inside the project .env file:

COMETA_ENDPOINT=COMETA_ENDPOINT

Create a new Ignition module:

import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

module.exports = buildModule("CounterBugModule", (m: any) => {
const counterBug = m.contract("CounterBug");

return { counterBug };
});

Deploy the contract and register it inside Cometa:

npx hardhat ignition  deploy ./ignition/modules/CounterBug.ts
npx hardhat cometa register --contract CounterBug --address ADDRESS

Create a new task for calling the contract:

import { task } from "hardhat/config";

task("increment", "Increment the CounterBug")
.addParam("address", "The address of the CounterBug contract")
.setAction(async (taskArgs, hre) => {
const ContractBug = await hre.ethers.getContractFactory("ContractBug");
const contractBug= ContractBug.attach(taskArgs.address);

const setterTx = await contractBug.increment();
await setterTx.wait(0);
});

Execute the task:

npx hardhat increment --address ADDRESS

Investigate failed messages to the contract

Via the =nil; CLI

To send a message to the increment() function of the contract:

nil wallet send-message COUNTER_BUG_ADDRESS increment --abi path/to/CounterBug.abi 

The command will produce the hash of the failed message to the contract. To investigate this message:

nil debug MESSAGE_HASH 

The output of the command should contain the entire message chain as well as the exact line where execution was reverted:

require(msg.sender == address(0));

Via Nil.js

Nil.js currently does not support the debug API for Cometa.

Via the Hardhat plugin

The Hardhat plugin currently does not support the debug API for Cometa.