Debugging Java Smart Contracts
Don’t debug, let your IDE do it for you
In this tutorial we will implement a simple contract, write unit tests for it, and run through a debugging process to find errors.
All you need to run this example is Java and an IDE. Simply create a Java project that depends on the AVM jar as a library. You can download the latest AVM build from the GitHub repository.
1. Writing the Smart Contract
Let’s start by creating the smart contract itself. Below is an example of a smart contract that can be used for a simple voting DApp.
- Only the owner of the contract (account deploying the contract) may add or remove members,
- Only members can introduce new proposals,
- Only members can vote on proposals,
- If more than 50% of members are in favor of a proposal and vote for it, the proposal will pass.
The static block in the contract is only executed once at deployment. We set the initial members, minimumQuorum, and owner in this block. Although we initiated our contract with a set of members, the owner can also add and remove members afterwards.
We keep track of the members and their proposals using an AionSet andAionMap. Each proposal can be accessed from the map using its unique identifier.
The main functions in the contract are:
- addProposal which allows a member to add a proposal description for a vote.
- vote which allows a member to vote on an available proposal by passing its Id. A proposal that has gotten majority of members’ votes will pass. Notice that a ProposalPassed event is generated to log the Id of the passed proposal.
2. Writing Unit Tests
We will use the AvmRule to write our test. AvmRule is a Junit Rule designed for testing contracts on an embedded AVM. It creates an in-memory representation of the Aion kernel and AVM. Every time we run a test, the built kernel and AVM instances are refreshed.
Before testing our contract, we need to deploy it to an in-memory Aion blockchain and we’ll use the AvmRule to accomplish this.
A. Instantiate the AvmRule
You can see that the rule takes in a boolean argument which enables/disables the debug mode. It’s good practice to write your tests with the debugger enabled. You can see how to debug your contract in the next section.
public AvmRule avmRule = new AvmRule(true);
Note: This line will instantiate an embedded AVM for each test method. If the rule is defined as a @classRule, only one instance of the AVM and kernel will be instantiated for the test class.
B. Getting Contract Bytes
Now we have to get the bytes that correspond to the in-memory representation of the contract jar. In order to get the bytes, we will use the getDappBytes method from the AvmRule.
getDappBytes takes the following parameters:
- Main class of the contract,
- Contract constructor arguments, which can be accessed and decoded in the static block,
- Other classes that need to be included in the DApp jar.
Note: classes in the org.aion.avm.userlib package must be explicitly passed in as parameters. We’ll see an example of this later.
public byte getDappBytes(Class> mainClass, byte arguments, Class>... otherClasses)
C. Deploying your contract
Deploying the contract can easily be done using the deploy function.
public ResultWrapper deploy(Address from, BigInteger value, byte dappBytes)
AvmRule also gives the ability to create accounts with initial balances in the Aion kernel.
Here’s how to deploy our Voting contract with a set of 3 members.
D. Calling Methods
We can call methods in the contract by:
- Encoding the method name and its arguments,
- Passing the encoded bytes to the AvmRule’s call method.
public ResultWrapper call(Address from, Address dappAddress, BigInteger value, byte transactionData)
As an example, let’s create a new proposal. We will validate the proposal by checking that the NewProposalAdded event is generated and the event topics and data are correct.
Now we’ll submit a proposal along with two votes for it. Since two distinct members voted for proposal with Id 0, the proposal should pass. Thus, we expect two different events to be generated for the last transaction- Voted and ProposalPassed.
You can also query a proposal’s status by its Id. You’ll see that the decoded data that is returned is true, meaning that the proposal has passed.
3. Debugging the Contract
It’s super easy to debug our contract — simply set the breaking points in the source code! Since we created our AvmRule with debugging enabled, the execution will be halted when the breaking point is reached. Let’s run through an example.
This is the state of the smart contract after deployment.
You can see that the contract has:
- 3 members,
- 0 proposals,
- minimumQuorum = 2.
You can inspect the contents of each collection as well. For instance — by calling the addProposal, you will be able to see the updated AionMap.
Let’s actually put the debugger to the test. We’ll be purposely creating a simple mistake in the way the proposal is evaluated as passed. I’ll modify the proposal passing criteria as shown below. Notice that the equals condition has been changed to less than or equal.
if (!votedProposal.isPassed && votedProposal.voters.size() <= minimumQuorum)
Now when the first owner submits their vote, the proposal will pass.
Let’s debug the method call and run through the function step by step.
You can see that, although minimumQuorum is equal to 2, the voter count for the proposal is only 1. Our modified if statement (from above) has been satisfied, and on line 48 the isPassed flag is set to true.
From there, you can easily identify where the bug was in your code.
If you’ve ever developed smart contracts for Ethereum, you’d know the pain of writing the contract in an unfamiliar, purpose-built language and debugging it.
Anybody familiar with Java, would feel right at home writing smart contracts using the AVM. Plus, all the debugging features in any IDE on the market can be used to test and debug a Java smart contract. Go ahead and give it a try yourself!