Are you one of those people who have used Remix and have not tried the debugger? If so, allow me, dear programmer, to walk you through the use of this tool.

The first part of this post shows how the debugger works when a transaction fails. The second part, uses it to view the local and state variables while scrolling around the code and while working with breakpoints.

For this journey, we’ll work with a smart contract that resembles Kickstarter — the website where you can post a project and the project’s fundraising goal and where people can fund your project. If the goal is reached you can withdraw their donations.

The most common way to to activate and load the debugger is to click the Debug button in the terminal after transaction is logged.

So, let’s begin with a problematic file.

pragma solidity >=0.4.22 <0.6.0;

contract Kickstarter {

enum State { Started, Completed } struct Project {

address owner;

string name;

uint goal;

State state;

} Project[] public projects; constructor() public {

} function createProject(string memory name, uint goal) public {

Project storage project = projects[projects.length];

project.name = name;

project.owner = msg.sender;

project.state = State.Started;

}

}

I’m setting up the Project struct and also making an array of Project structs called projects.

To follow along, create a new file in Remix’s File Explorers module and copy this code into the new editor tab in the main panel of Remix and compile it with the Solidity Compiler module.

But before we get to the debugger — we have a compilation warning that is complaining about an unused parameter on line 17:

Yes, I know that this is an article about the Debugger — but fixing mistakes doesn’t only happen in there. So I’ll heed the advice of the compilation warning and I’ll add the project.goal assignment in the code below.

function createProject(string memory name, uint goal) public {

Project storage project = projects[projects.length];

project.name = name;

project.owner = msg.sender;

project.state = State.Started

project.goal = goal; // new line }

}

So moving on… lets deploy this contract.

At deployment there are no errors.

I haven’t activated the Debugger module in the Plugin Manager— but if I click the Debug button in the terminal (see the image above) — the debugger will get activated. But let’s not do that yet.

Rather, let’s interact with the createProject function — adding a project name and a goal and then clicking the createProject button.

Now in the terminal — there is a red X — indicating that we’ve got problems with this transaction. It also tries to give us some help:

transact to Kickstarter.createProject errored: VM error: invalid opcode. invalid opcode

Mmmmm invalid opcode… that smells like some low level warning to a something in the contract. But I need to know what is happening and where it is happening. So let’s click the debug button.

In the icon panel, on the left side of Remix, the debugger is open. In the editor, line 18 is highlighted. So something is wrong here.

In the debugger module, check out the panels: Solidity Locals & Solidity State. The Solidity Locals panel show that the parameters getting loaded.

You can move around the code by dragging the horizontal slider around. As you slide it back and forth, you’ll see what the current opcode is. Also try clicking the step back and step into buttons to go through the program opcode by opcode. You can also add breakpoints in the gutter of the Editor and then jump to those breakpoints using the Jump to Next Breakpoint buttons.

In the Solidity Locals panel, you’ll see that the parameters do get loaded — but in the Solidity State panel, the projects array always has a length of 0.

This means that the projects array is not getting added to. And so there must be a problem with how I’m trying to add to the projects array.

Indeed in Solidity, you need to expand the slots in the array to add to it.

projects.length++;

Now let’s move on to using the Debugger to explore what is happening when there are no errors but the contract does not work as expected.

Seeing an Underflow in all its Dangerous Glory

Below is the next draft of the kickstarter contract. In this version of the contract, we’ll use the the debugger to see an underflow take place that will allow for a malicious project owner to steal funds stored in the contract.

Here’s the code:

pragma solidity >=0.4.22 <0.6.0;

contract Kickstarter {

enum State { Started, Completed }



struct Project {

address owner;

string name;

uint goal;

uint fundsAvailable; // added

uint amountContributed; // added

State state;

mapping(address => uint) funders; // added

}



Project[] public projects;



constructor() public {

} function createProject(string memory name, uint goal) public {

projects.length++;

Project storage project = projects[projects.length - 1];

project.name = name;

project.goal = goal;

project.owner = msg.sender;

project.state = State.Started;

}

// a payable function for adding funds to a project

function fundProject(uint projectId) payable public {

Project storage project = projects[projectId];

project.funders[msg.sender] += msg.value;

project.amountContributed += msg.value;

project.fundsAvailable += msg.value;



if (project.amountContributed >= project.goal) {

project.state = State.Completed;

}

}



// this function is here because we can't use web3 when using the JSVM

function getContractBalance() public view returns(uint balance){

return address(this).balance;

} // this function has a major security hole.

function withdraw(uint projectId, uint amountToWithdraw) public {

Project storage project = projects[projectId];

require(msg.sender == project.owner);

require(project.amountContributed >= project.goal);



project.fundsAvailable -= amountToWithdraw;



// for demo purposes ONLY - do not send funds like this...

msg.sender.send(amountToWithdraw);

}

}

So lets compile and then deploy this contract to the JavaScript VM.

Oh but when we compile — we get this warning about the security problem in the file:

Well that was nice of the compiler to tell us we are a knucklehead. But actually we want to be knuckleheads — so we shall continue on this path. So deploy the contract to the JavaScript VM.

Now let’s create 2 projects with dramatically different goals that are owned by 2 different accounts. Then we’ll fund the projects and then we’ll overdraw the modest project and will check its balance.

So to do this in a step by step fashion:

Deploy the contract

Create a project — I’ve called my modest project Tulip and have given it a goal of 100. (note the 100 has no unit — but its in wei)

Fund this project to meet it’s goal. Fill in the Value field at the top of the Deploy & Run module and also put the projectId in the fundProject function. The projectId is the position of the project in the projects array — so for the 1st project the position is 0.

Switch account at the top of the Deploy & Run module. Make a new project with a much larger amount and fund it with a lot of wei.

Clicking on getContractBalance will show this:

Now go back to the 1st Account and withdraw 200.

This project was funded with 111 so we are trying to withdraw more than is in the project and yet the transaction is successful.

Let’s click the debugger button in the console. The debugger loads:

Go down to the horizontal slider and by pulling to the right or left you’ll highlight different lines in the editor. You can also click the Step Into button to move through the code Op code by Op code.

Open up the Solidity State panel and the projects array to see what is going on there.

Slide the slider until you get to this line:

msg.sender.send(amountToWithdraw);

At this point, check what is the balance (fundsAvailable) of project 0.

What we have is a really huge number for fundsAvailable (115792089237316195423570985008687907853269984665640564039457584007913129639847) — but the amount contributed is 111.

We have been able to perform the illustrious underflow. The underflow happened by taking fundsAvailable below 0 or at least that’s what it would do if it were the type int. However fundsAvailable is a uint. When a uint goes below 0, it rebounds to it’s maximum value.

Now that our fundsAvailable is huge, we could drain off the rest of the contract’s funds.

Zero in on the line where the value of fundsAvailable jumps from 111 to 115792089237316195423570985008687907853269984665640564039457584007913129639847.

By carefully dragging the slider or clicking the step into button, you can see the specific opcode where the problem is occurring. Of course opcodes have the smell or perfume of low level information — which may not be helpful to you.

The line in question is:

project.fundsAvailable -= amountToWithdraw;

The hole in my logic when I coded the contract was that I didn’t make sure that the amountToWithdraw can never be more than the fundsAvailable.

Furthermore, the compiler warning on the following line is there because this method of sending funds doesn’t revert if there is a problem.

msg.sender.send(amountToWithdraw);

So it is time to research different methods of sending funds. But that is the topic of another walk in the code garden. We could also automate this test by doing this with Remix unit tests. Please see this post about using Remix Unit Tests. Also please checkout the Debugger Docs.

Hopefully with this walk through of the Debugger, you’ll be able to squash some bugs in the garden of your code.

Special thanks to Iuri Matias.