Lesson 13 of 13
In Progress

Tips and Best Practices for Building DApps with Truffle

Truffle is a popular development framework for building decentralized applications (DApps) on the Ethereum blockchain. As you start building your own DApps with Truffle, here are some tips and best practices to keep in mind:

Use the Latest Version of Truffle

As with any software, it’s important to keep your Truffle version up to date. New versions of Truffle often include bug fixes, performance improvements, and new features. To update Truffle, run the following command:

npm update -g truffle

Follow Solidity Best Practices

Solidity is the programming language used to write Ethereum smart contracts. When writing Solidity code, it’s important to follow best practices to ensure the security and reliability of your contracts. Here are some tips:

  • Use the latest version of Solidity. As with Truffle, new versions of Solidity often include bug fixes and improvements.
  • Use explicit visibility (i.e. public, internal, private) for all functions and variables. This helps prevent accidental contract breaches and improves code readability.
  • Use the assert and require functions to validate input and enforce invariants.
  • Use the view and pure functions to mark functions that do not modify contract state. This can improve contract performance and reduce gas costs.

Test Your Contracts Thoroughly

Testing is an important part of the development process, and it’s especially important when building DApps on the blockchain. Truffle provides a powerful testing framework that makes it easy to write and run tests for your contracts. Some best practices for testing your contracts include:

  • Write tests for all functions in your contracts. This includes both positive and negative test cases.
  • Use Truffle’s utility functions (e.g. web3.eth.getBalance, web3.eth.getTransactionReceipt) to inspect the state of your contracts and the Ethereum network during tests.
  • Use the Truffle Debugger to debug failing tests and understand what went wrong.

Use a Version Control System

As with any software project, it’s important to use a version control system (e.g. Git) to track changes to your code. This helps you revert to previous versions if something goes wrong, and it makes it easier to collaborate with other developers.

Use a Linter

A linter is a tool that helps you enforce coding style and catch syntax errors in your code. For Solidity, we recommend using the Solidity Linter. To install it, run the following command:

npm install -g solium

Then, you can run solium from the command line to lint your Solidity code. For example:

solium -d contracts/

This will lint all Solidity files in the contracts/ directory.

Use a Security Audit Tool

Security is critical when building DApps on the blockchain. To ensure the security of your contracts, we recommend using a security audit tool like Mythril or Oyente. These tools can help you identify potential vulnerabilities in your contracts, such as reentrancy attacks or uninitialized storage variables.

Conclusion

By following these tips and best practices, you can build high-quality DApps with Truffle that are reliable, secure, and scalable. With a little effort and attention to detail, you can create DApps that are ready for deployment on the Ethereum mainnet.

As you continue to build DApps with Truffle, keep an eye out for new best practices and tools that can help you improve your workflow and code quality. With the right tools and techniques, building DApps with Truffle can be a fun and rewarding experience.

If you have any questions or need further assistance, you can refer to the Truffle documentation (https://truffleframework.com/docs/) or ask for help in the Truffle community (https://truffleframework.com/community). Happy coding!

Exercises

To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.

Write a Solidity contract that defines a “Bank” contract with the following functionality:
-The contract has an owner and a balance.
-The owner can deposit ether into the contract by calling the deposit function.
-The owner can withdraw ether from the contract by calling the withdraw function.
-The contract has a transfer function that allows the owner to send ether to another address.
-The contract has a kill function that allows the owner to destroy the contract and withdraw all remaining balance.

pragma solidity ^0.6.0;

contract Bank {
    address public owner;
    uint public balance;

    constructor() public {
        owner = msg.sender;
    }

    function deposit() public payable {
        require(msg.value > 0, "Cannot deposit 0 or less ether.");
        balance += msg.value;
    }

    function withdraw(uint amount) public {
        require(amount <= balance, "Insufficient balance.");
        require(amount > 0, "Cannot withdraw 0 or less ether.");
        balance -= amount;
        msg.sender.transfer(amount);
    }

    function transfer(address recipient, uint amount) public {
        require(amount <= balance, "Insufficient balance.");
        require(amount > 0, "Cannot transfer 0 or less ether.");
        balance -= amount;
        recipient.transfer(amount);
    }

    function kill() public {
        require(balance > 0, "Cannot kill contract with 0 balance.");
        owner.transfer(balance);
        selfdestruct(owner);
    }
}

Write a Truffle test for the Bank contract from Exercise 1 that does the following:
-Deploys a new instance of the Bank contract.
-Calls the deposit function to deposit 1 ether into the contract.
-Calls the withdraw function to withdraw 0.5 ether from the contract.
-Calls the transfer function to send 0.25 ether to another address.
-Calls the kill function to destroy the contract and withdraw the remaining balance.

const Bank = artifacts.require("Bank");

contract("Bank", () => {
    it("should allow the owner to deposit, withdraw, transfer, and kill the contract", async () => {
        const bank = await Bank.new();
        const owner = await bank.owner();
        const recipient = "0x1234567890abcdef1234567890abcdef12345678";

        // Deposit 1 ether
        await web3.eth.sendTransaction({
            to: bank.address,
            from: owner,
            value: web3.utils.toWei("1", "ether")
        });
        assert.equal((await bank.balance()).toString(), web3.utils.toWei("1", "ether"));

        // Withdraw 0.5 ether
        await bank.withdraw(web3.utils.toWei("0.5", "ether"), { from: owner });
        assert.equal((await bank.balance()).toString(), web3.utils.toWei("0.5", "ether"));

        // Transfer 0.25 ether
        await bank.transfer(recipient, web3.utils.toWei("0.25", "ether"), { from: owner });
        assert.equal((await bank.balance()).toString(), web3.utils.toWei("0.25", "ether"));

        // Kill contract
        const initialBalance = await web3.eth.getBalance(owner);
        await bank.kill({ from: owner });
        assert.equal((await web3.eth.getBalance(owner)).toString(), initialBalance.add(web3.utils.toBN(web3.utils.toWei("0.25", "ether"))).toString());
    });
});

Write a Solidity contract that defines a “Voting” contract with the following functionality:
-The contract has a list of candidates, represented as strings.
-The contract has a function that allows a voter to cast a vote for a specific candidate.
-The contract has a function that returns the number of votes received by each candidate.
-The contract has a function that allows the owner to add new candidates to the list.

pragma solidity ^0.6.0;

import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/utils/SafeMath.sol";

contract Bank {
    using SafeMath for uint;

    address public owner;
    mapping(address => uint) public balances;
    uint public totalBalance;

    constructor() public {
        owner = msg.sender;
    }

    function deposit() public payable {
        require(msg.value > 0, "Cannot deposit 0 or less ether.");
        balances[msg.sender] = balances[msg.sender].add(msg.value);
        totalBalance = totalBalance.add(msg.value);
    }

    function withdraw(uint amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance.");
        require(amount > 0, "Cannot withdraw 0 or less ether.");
        balances[msg.sender] = balances[msg.sender].sub(amount);
        totalBalance = totalBalance.sub(amount);
        msg.sender.transfer(amount);
    }

    function transfer(address recipient, uint amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance.");
        require(amount > 0, "Cannot transfer 0 or less ether.");
        balances[msg.sender] = balances[msg.sender].sub(amount);
        balances[recipient] = balances[recipient].add(amount);
    }

    function kill() public {
        require(totalBalance > 0, "Cannot kill contract with 0 balance.");
        require(msg.sender == owner, "Only the owner can kill the contract.");
        owner.transfer(totalBalance);
        selfdestruct(owner);
    }
}

Write a Truffle test for the Voting contract from Exercise 3 that does the following:
-Deploys a new instance of the Voting contract with 3 candidates (Alice, Bob, and Charlie).
-Calls the vote function twice to cast votes for Alice and Bob.
-Calls the getVotes function to retrieve the vote count for each candidate.
-Asserts that Alice has 1 vote and Bob has 1 vote.
-Calls the addCandidate function to add a new candidate (Dave).
-Calls the vote function to cast a vote for Dave.
-Calls the getVotes function to retrieve the vote count for each candidate.
-Asserts that Dave has 1 vote.

pragma solidity ^0.6.0;

import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/utils/SafeMath.sol";

contract Bank {
    using SafeMath for uint;

    address public owner;
    mapping(address => uint) public balances;
    uint public totalBalance;

    constructor() public {
        owner = msg.sender;
    }

    function deposit() public payable {
        require(msg.value > 0, "Cannot deposit 0 or less ether.");
        balances[msg.sender] = balances[msg.sender].add(msg.value);
        totalBalance = totalBalance.add(msg.value);
    }

    function withdraw(uint amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance.");
        require(amount > 0, "Cannot withdraw 0 or less ether.");
        balances[msg.sender] = balances[msg.sender].sub(amount);
        totalBalance = totalBalance.sub(amount);
        msg.sender.transfer(amount);
    }

    function transfer(address recipient, uint amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance.");
        require(amount > 0, "Cannot transfer 0 or less ether.");
        balances[msg.sender] = balances[msg.sender].sub(amount);
        balances[recipient] = balances[recipient].add(amount);
    }

    function kill() public {
        require(totalBalance > 0, "Cannot kill contract with 0 balance.");
        require(msg.sender == owner, "Only the owner can kill the contract.");
        owner.transfer(totalBalance);
        selfdestruct(owner);
    }
}

Write a Solidity contract that defines a “Token” contract with the following functionality:
-The contract has a name, symbol, and total supply.
-The contract has a function that allows the owner to mint new tokens and add them to their balance.
-The contract has a function that allows a user to transfer tokens to another address.
-The contract has a function that returns the balance of a specific address.

const Bank = artifacts.require("Bank");

contract("Bank", () => {
    it("should allow the owner to deposit, withdraw, transfer, and kill the contract", async () => {
        const bank = await Bank.new();
        const owner = await bank.owner();
        const recipient = "0x1234567890abcdef1234567890abcdef12345678";

        // Deposit 1 ether
        await web3.eth.sendTransaction({
            to: bank.address,
            from: owner,
            value: web3.utils.toWei("1", "ether")
        });
        assert.equal((await bank.balances(owner)).toString(), web3.utils.toWei("1", "ether"));
        assert.equal((await bank.totalBalance()).toString(), web3.utils.toWei("1", "ether"));

        // Withdraw 0.5 ether
        await bank.withdraw(web3.utils.toWei("0.5", "ether"), { from: owner });
        assert.equal((await bank.balances(owner)).toString(), web3.utils.toWei("0.5", "ether"));
        assert.equal((await bank.totalBalance()).toString(), web3.utils.toWei("0.5", "ether"));

        // Transfer 0.25 ether
        await bank.transfer(recipient, web3.utils.toWei("0.25", "ether"), { from: owner });
        assert.equal((await bank.balances(owner)).toString(), web3.utils.toWei("0.25", "ether"));
        assert.equal((await bank.balances(recipient)).toString(), web3.utils.toWei("0.25", "ether"));
        assert.equal((await bank.totalBalance()).toString(), web3.utils.toWei("0.5", "ether"));

        // Kill the contract
        await bank.kill({ from: owner });
        assert.equal(await web3.eth.getBalance(bank.address), 0);
    });
});