Skip to main content

Debugging smart contracts

=nil; comes equipped with the Cometa service which is a tool for storing contract metadata and analyzing transactions. If a transaction 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 transactions to the contract
Playground integration

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

Set the correct Cometa endpoint

To use the Cometa service with =nil; developer tools, it is necessary to to set the correct endpoint to it:

  • For the =nil; CLI, the endpoint should be set in the CLI config file:
cometa_endpoint: COMETA_ENDPOINT
  • For Nil.js, the endpoint should be set when creating an instance of CometaService:
const service = new CometaService({
transport: new HttpTransport({
endpoint: COMETA_ENDPOINT,
}),
});

Via the =nil; testnet

When using the =nil; testnet, the Cometa service endpoint is the same as the RPC endpoint.

Via running Cometa locally

It is also possible to run Cometa locally.

First, enter the Nix development environment:

nix develop

Then, build the faucet binary:

make cometa

Launch the Cometa service at port 8528:

./build/bin/cometa run --use-badger

It should now be possible to send requests to the Cometa service at http://127.0.0.1:8528.

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 smart-account 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 smart-account 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,
HttpTransport,
PublicClient,
generateSmartAccount,
waitTillCompleted,
} 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 smartAccount = await generateSmartAccount({
shardId: 1,
rpcEndpoint: RPC_ENDPOINT,
faucetEndpoint: FAUCET_ENDPOINT,
});

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 smartAccount.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 smartAccount.sendTransaction({
to: address,
functionName: "increment",
abi: compilationResult.abi as unknown as Abi,
feeCredit: 300_000n,
});

await waitTillCompleted(client, incrementHash);

Investigate failed transactions to the contract

Via the =nil; CLI

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

nil smart-account send-transaction COUNTER_BUG_ADDRESS increment --abi path/to/CounterBug.abi 

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

nil debug TRANSACTION_HASH 

The output of the command should contain the entire transaction 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.