Aion University

So you want to be a DApp developer?


Well you've come to the right place. You'll find comprehensive guides and documentation to help you start developing with Aion as quickly as possible, as well as support if you get stuck. Let's jump right in!

Let's DApp

🎓 Debug your Java Smart Contract

Don’t debug, let your IDE do it for you

Guide Level: Intermediate

You are a developer who

  • Is experienced with Java
  • Is familiar with smart contracts
  • Understands debugging processes

Get Started

Requirements

This guide will walk you through on how to implement a simple Java contract, write unit tests for it, and run through a debugging process to find those bugs!

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. The Smart Contract

Contract Logic

  • 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.

Main Functions

  • addProposal - allows a member to add a proposal description for a vote.
  • vote - 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
package org.aion.avm;

import org.aion.avm.api.ABIDecoder;
import org.aion.avm.api.Address;
import org.aion.avm.api.BlockchainRuntime;
import org.aion.avm.userlib.AionMap;
import org.aion.avm.userlib.AionSet;

public class Voting {

    private static AionSet <Address> members = new AionSet<>();
    private static AionMap <Integer, Proposal> proposals = new AionMap<>();
    private static Address owner;
    private static int minimumQuorum;

    static {
        Address[] arg = (Address[]) ABIDecoder.decodeOneObject(BlockchainRuntime.getData());
        for(Address addr: arg){
            members.add(addr);
        }
        updateQuorum();
        owner = BlockchainRuntime.getCaller();
    }

    public static byte[] main(){
        return ABIDecoder.decodeAndRunWithClass(Voting.class, BlockchainRuntime.getData());
    }

    public static void addProposal(String description){
        BlockchainRuntime.require(isMember(BlockchainRuntime.getCaller()));

        Proposal proposal = new Proposal(description, BlockchainRuntime.getCaller());
        int proposalId = proposals.size();
        proposals.put(proposalId, proposal);

        BlockchainRuntime.log("NewProposalAdded".getBytes(), Integer.toString(proposalId).getBytes(), BlockchainRuntime.getCaller().unwrap(), description.getBytes());
    }

    public static void vote(int proposalId){
        BlockchainRuntime.require(isMember(BlockchainRuntime.getCaller()) && proposals.containsKey(proposalId));

        Proposal votedProposal = proposals.get(proposalId);
        votedProposal.voters.add(BlockchainRuntime.getCaller());

        BlockchainRuntime.log("Voted".getBytes(), Integer.toString(proposalId).getBytes(), BlockchainRuntime.getCaller().unwrap());

        if (!votedProposal.isPassed && votedProposal.voters.size() == minimumQuorum){
            votedProposal.isPassed = true;
            BlockchainRuntime.log("ProposalPassed".getBytes(), Integer.toString(proposalId).getBytes());
        }
    }

    public static void addMember(Address newMember){
        onlyOwner();
        members.add(newMember);
        updateQuorum();
        BlockchainRuntime.log("MemberAdded".getBytes(), newMember.unwrap());
    }

    public static void removeMember(Address member){
        onlyOwner();
        members.remove(member);
        updateQuorum();
        BlockchainRuntime.log("MemberRemoved".getBytes(), member.unwrap());
    }

    public static String getProposalDescription(int proposalId){
        return proposals.containsKey(proposalId)? proposals.get(proposalId).description : null;
    }

    public static Address getProposalOwner(int proposalId){
        return proposals.containsKey(proposalId)? proposals.get(proposalId).owner : null;
    }

    public static boolean hasProposalPassed(int proposalId){
        return proposals.containsKey(proposalId) && proposals.get(proposalId).isPassed;
    }

    public static Integer getMinimumQuorum() { return minimumQuorum; }

    public static boolean isMember(Address address){
        return members.contains(address);
    }

    private static void onlyOwner(){
        BlockchainRuntime.require(owner.equals(BlockchainRuntime.getCaller()));
    }

    private static void updateQuorum(){ minimumQuorum =  members.size()/2 + 1; }

    private static class Proposal {
        String description;
        Address owner;
        boolean isPassed;
        AionSet<Address> voters = new AionSet<>();

        Proposal(String description, Address owner) {
            this.description = description;
            this.owner = owner;
        }
    }
}

Notes
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 and AionMap. Each proposal can be accessed from the map using its unique identifier.

2. Create Unit Tests

We will be using AvmRule to write our tests.

AvmRule

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 AvmRule to accomplish this.

2.1 Instantiate AvmRule

Notice that the rule takes in a boolean argument - which enables/disables the debug mode. It’s good practice to write all your tests with the debugger enabled.

@Rule
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.

2.2 ResultWrapper

You can deploy your contract and call its methods using AvmRule deploy and call functions.
The output of these functions will be returned as type ResultWrapper which will gives you the following info:

  • getReceiptStatus()
  • getDappAddress()
  • getDecodedReturnData()
  • getLogs()
// Example
// calling the contract
AvmRule.ResultWrapper result = avmRule.call(members[0], dappAddress, BigInteger.ZERO, txData);

// validate the transaction was successful
Assert.assertTrue(result.getReceiptStatus().isSuccess());

2.3 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:

  1. Main class of the contract
  2. Contract constructor arguments, which can be accessed and decoded in the static block
  3. Other classes that are necessary from the DApp jar

Note - classes in the org.aion.avm.userlib package must be explicitly passed in as parameters.

public byte[] getDappBytes(Class<?> mainClass, 
                           byte[] arguments, 
                           Class<?>... otherClasses)

2.4 Deploy the Contract

Deploying the contract is easy and can be done so by 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.

Let's deploy the Voting contract (from above), with a set of 3 members.

public class VotingContractTest {

    @Rule
    public AvmRule avmRule = new AvmRule(true);

    public Address dappAddress;
    public Address owner = avmRule.getPreminedAccount();
    public Address[] members = new Address[3];

    @Before
    public void setup(){
        for(int i =0; i< members.length; i++){
            // create accounts with iniital balance
            members[i] = avmRule.getRandomAddress(BigInteger.valueOf(10_000_000L));
        }
        // encode members array as an argument
        byte[] deployArgument = ABIEncoder.encodeOneObject(members);
        // get the bytes representing the in memory jar
        byte[] dappBytes = avmRule.getDappBytes(Voting.class, deployArgument, AionSet.class, AionMap.class);
        //deploy and get the contract address
        dappAddress = avmRule.deploy(owner, BigInteger.ZERO, dappBytes).getDappAddress();
    }
}

2.5 Calling Methods

Call the functions in your contract by

  1. Encoding the method name and its arguments
  2. Then, passing the encoded bytes to the AvmRule’s call method.
public ResultWrapper call(Address from, 
                          Address dappAddress, 
                          BigInteger value, 
                          byte[] transactionData)

For example, let's create a new proposal. We will validate the proposal by checking that the NewProposalAdded event gets generated, and the event topics and data are correct ✓

@Test
public void addProposalTest(){
  String description = "new proposal description";
  // encode method and its arguments
  byte[] txData = ABIEncoder.encodeMethodArguments("addProposal", description);
  // call the contract
  AvmRule.ResultWrapper result = avmRule.call(members[0], dappAddress, BigInteger.ZERO, txData);
  // validate the transaction was successful
  Assert.assertTrue(result.getReceiptStatus().isSuccess());

  // validate the event is generated
  assertEquals(1, result.getLogs().size());
  IExecutionLog log = result.getLogs().get(0);
  // validate the topics and data
  assertArrayEquals(HashUtils.sha256("NewProposalAdded".getBytes()), log.getTopics().get(0));
  assertArrayEquals(HashUtils.sha256(Integer.toString(0).getBytes()), log.getTopics().get(1));
  assertArrayEquals(HashUtils.sha256(members[0].unwrap()), log.getTopics().get(2));
  assertArrayEquals(description.getBytes(), log.getData());
}

Now, let's submit a proposal along with two votes for it. Since two distinct members voted for proposal (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.

@Test
public void voteTest(){
  String description = "new proposal description";
  byte[] txData = ABIEncoder.encodeMethodArguments("addProposal", description);
  AvmRule.ResultWrapper result = avmRule.call(members[0], dappAddress, BigInteger.ZERO, txData);
  Assert.assertTrue(result.getReceiptStatus().isSuccess());
  assertEquals(1, result.getLogs().size());
  //vote #1
  txData = ABIEncoder.encodeMethodArguments("vote", 0);
  result = avmRule.call(members[1], dappAddress, BigInteger.ZERO, txData);
  Assert.assertTrue(result.getReceiptStatus().isSuccess());
  assertEquals(1, result.getLogs().size());
  //vote #2
  txData = ABIEncoder.encodeMethodArguments("vote", 0);
  result = avmRule.call(members[2], dappAddress, BigInteger.ZERO, txData);
  Assert.assertTrue(result.getReceiptStatus().isSuccess());
  Assert.assertEquals(2, result.getLogs().size());
  //validate that the proposal is stored as passed
  txData = ABIEncoder.encodeMethodArguments("hasProposalPassed", 0);
  result = avmRule.call(members[2], dappAddress, BigInteger.ZERO, txData);
  //decode the return data as boolean
  Assert.assertTrue((boolean)result.getDecodedReturnData());
}

3. Debug 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.

Take a look at 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 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.

Take a look as we 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.

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!

Need Help?

If you get stuck, try searching these docs 👆 or head over to our Gitter channels or StackOverflow for answers to some common questions.

Written by Shidokht Hejazi