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.

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.