Building Ethereum DApps: Whitelisting & Testing a Story DAO

Share this article

Building DApps with Ethereum: Writing DAO Code

In part 3 of this tutorial series on building DApps with Ethereum, we built and deployed our token to the Ethereum testnet Rinkeby. In this part, we’ll start writing the Story DAO code.

We’ll use the conditions laid out in the intro post to guide us.

Contract Outline

Let’s create a new contract, StoryDao.sol, with this skeleton:

pragma solidity ^0.4.24;

import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol";

contract StoryDao is Ownable {
    using SafeMath for uint256;

    mapping(address => bool) whitelist;
    uint256 public whitelistedNumber = 0;
    mapping(address => bool) blacklist;
    event Whitelisted(address addr, bool status);
    event Blacklisted(address addr, bool status);

    uint256 public daofee = 100; // hundredths of a percent, i.e. 100 is 1%
    uint256 public whitelistfee = 10000000000000000; // in Wei, this is 0.01 ether

    event SubmissionCommissionChanged(uint256 newFee);
    event WhitelistFeeChanged(uint256 newFee);

    uint256 public durationDays = 21; // duration of story's chapter in days
    uint256 public durationSubmissions = 1000; // duration of story's chapter in entries

    function changedaofee(uint256 _fee) onlyOwner external {
        require(_fee < daofee, "New fee must be lower than old fee.");
        daofee = _fee;
        emit SubmissionCommissionChanged(_fee);
    }

    function changewhitelistfee(uint256 _fee) onlyOwner external {
        require(_fee < whitelistfee, "New fee must be lower than old fee.");
        whitelistfee = _fee;
        emit WhitelistFeeChanged(_fee);
    }

    function lowerSubmissionFee(uint256 _fee) onlyOwner external {
        require(_fee < submissionZeroFee, "New fee must be lower than old fee.");
        submissionZeroFee = _fee;
        emit SubmissionFeeChanged(_fee);
    }

    function changeDurationDays(uint256 _days) onlyOwner external {
        require(_days >= 1);
        durationDays = _days;
    }

    function changeDurationSubmissions(uint256 _subs) onlyOwner external {
        require(_subs > 99);
        durationSubmissions = _subs;
    }
}

We’re importing SafeMath to have safe calculations again, but this time we’re also using Zeppelin’s Ownable contract, which lets someone “own” the story and execute certain admin-only functions. Simply saying that our StoryDao is Ownable is enough; feel free to inspect the contract to see how it works.

We also use the onlyOwner modifier from this contract. Function modifiers are basically extensions, plugins for functions. The onlyOwner modifier looks like this:

modifier onlyOwner() {
  require(msg.sender == owner);
  _;
}

When onlyOwner is added to a function, then that function’s body is pasted into the part where the _; part is, and everything before it executes first. So by using this modifier, the function automatically checks if the message sender is also the owner of the contract and then continues as usual if so. If not, it crashes.

By using the onlyOwner modifier on the functions that change the fees and other parameters of our story DAO, we make sure that only the admin can do these changes.

Testing

Let’s test the initial functions.

Create the folder test if it doesn’t exist. Then inside it, create the files TestStoryDao.sol and TestStoryDao.js. Because there’s no native way to test for exceptions in Truffle, also create helpers/expectThrow.js with the content:

export default async promise => {
    try {
      await promise;
    } catch (error) {
      const invalidOpcode = error.message.search('invalid opcode') >= 0;
      const outOfGas = error.message.search('out of gas') >= 0;
      const revert = error.message.search('revert') >= 0;
      assert(
        invalidOpcode || outOfGas || revert,
        'Expected throw, got \'' + error + '\' instead',
      );
      return;
    }
    assert.fail('Expected throw not received');
  };

Note: Solidity tests are generally used to test low-level, contract-based functions, the internals of a smart contract. JS tests are generally used to test if the contract can be properly interacted with from the outside, which is something our end users will be doing.

In TestStoryDao.sol, put the following content:

pragma solidity ^0.4.24;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/StoryDao.sol";

contract TestStoryDao {

    function testDeploymentIsFine() public {
        StoryDao sd = StoryDao(DeployedAddresses.StoryDao());

        uint256 daofee = 100; // hundredths of a percent, i.e. 100 is 1%
        uint256 whitelistfee = 10000000000000000; // in Wei, this is 0.01 ether

        uint256 durationDays = 21; // duration of story's chapter in days
        uint256 durationSubmissions = 1000; // duration of story's chapter in entries

        Assert.equal(sd.daofee(), daofee, "Initial DAO fee should be 100");
        Assert.equal(sd.whitelistfee(), whitelistfee, "Initial whitelisting fee should be 0.01 ether");
        Assert.equal(sd.durationDays(), durationDays, "Initial day duration should be set to 3 weeks");
        Assert.equal(sd.durationSubmissions(), durationSubmissions, "Initial submission duration should be set to 1000 entries");
    }
}

This checks that the StoryDao contract gets deployed properly with the right numbers for fees and duration. The first line makes sure it’s deployed by reading it from the list of deployed addresses, and last section does some assertions — checking that a claim is true or false. In our case, we’re comparing numbers to initial values of the deployed contract. Whenever it’s “true”, the Assert.equals part will emit an event that says “True”, which is what Truffle is listening for when testing.

In TestStoryDao.js, put the following content:

import expectThrow from './helpers/expectThrow';

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

contract('StoryDao Test', async (accounts) => {

    it("should make sure environment is OK by checking that the first 3 accounts have over 20 eth", async () =>{
        assert.equal(web3.eth.getBalance(accounts[0]).toNumber() > 2e+19, true, "Account 0 has more than 20 eth");
        assert.equal(web3.eth.getBalance(accounts[1]).toNumber() > 2e+19, true, "Account 1 has more than 20 eth");
        assert.equal(web3.eth.getBalance(accounts[2]).toNumber() > 2e+19, true, "Account 2 has more than 20 eth");
    });

    it("should make the deployer the owner", async () => {
        let instance = await StoryDao.deployed();
        assert.equal(await instance.owner(), accounts[0]);
    });

    it("should let owner change fee and duration", async () => {
        let instance = await StoryDao.deployed();

        let newDaoFee = 50;
        let newWhitelistFee = 1e+10; // 1 ether
        let newDayDuration = 42;
        let newSubsDuration = 1500;

        instance.changedaofee(newDaoFee, {from: accounts[0]});
        instance.changewhitelistfee(newWhitelistFee, {from: accounts[0]});
        instance.changedurationdays(newDayDuration, {from: accounts[0]});
        instance.changedurationsubmissions(newSubsDuration, {from: accounts[0]});

        assert.equal(await instance.daofee(), newDaoFee);
        assert.equal(await instance.whitelistfee(), newWhitelistFee);
        assert.equal(await instance.durationDays(), newDayDuration);
        assert.equal(await instance.durationSubmissions(), newSubsDuration);
    });

    it("should forbid non-owners from changing fee and duration", async () => {
        let instance = await StoryDao.deployed();

        let newDaoFee = 50;
        let newWhitelistFee = 1e+10; // 1 ether
        let newDayDuration = 42;
        let newSubsDuration = 1500;

        await expectThrow(instance.changedaofee(newDaoFee, {from: accounts[1]}));
        await expectThrow(instance.changewhitelistfee(newWhitelistFee, {from: accounts[1]}));
        await expectThrow(instance.changedurationdays(newDayDuration, {from: accounts[1]}));
        await expectThrow(instance.changedurationsubmissions(newSubsDuration, {from: accounts[1]}));
    });

    it("should make sure the owner can only change fees and duration to valid values", async () =>{
        let instance = await StoryDao.deployed();

        let invalidDaoFee = 20000;
        let invalidDayDuration = 0;
        let invalidSubsDuration = 98;

        await expectThrow(instance.changedaofee(invalidDaoFee, {from: accounts[0]}));
        await expectThrow(instance.changedurationdays(invalidDayDuration, {from: accounts[0]}));
        await expectThrow(instance.changedurationsubmissions(invalidSubsDuration, {from: accounts[0]}));
    })
});

In order for our tests to successfully run, we also need to tell Truffle that we want the StoryDao deployed — because it’s not going to do it for us. So let’s create 3_deploy_storydao.js in migrations with content almost identical to the previous migration we wrote:

var Migrations = artifacts.require("./Migrations.sol");
var StoryDao = artifacts.require("./StoryDao.sol");

module.exports = function(deployer, network, accounts) {
  if (network == "development") {
    deployer.deploy(StoryDao, {from: accounts[0]});
  } else {
    deployer.deploy(StoryDao);
  }
};

At this point, we should also update (or create, if it’s not present) a package.json file in the root of our project folder with the dependencies we needed so far and may need in the near future:

{
  "name": "storydao",
  "devDependencies": {
    "babel-preset-es2015": "^6.18.0",
    "babel-preset-stage-2": "^6.24.1",
    "babel-preset-stage-3": "^6.17.0",
    "babel-polyfill": "^6.26.0",
    "babel-register": "^6.23.0",
    "dotenv": "^6.0.0",
    "truffle": "^4.1.12",
    "openzeppelin-solidity": "^1.10.0",
    "openzeppelin-solidity-metadata": "^1.2.0",
    "openzeppelin-zos": "",
    "truffle-wallet-provider": "^0.0.5",
    "ethereumjs-wallet": "^0.6.0",
    "web3": "^1.0.0-beta.34",
    "truffle-assertions": "^0.3.1"
  }
}

And a .babelrc file with the content:

{
  "presets": ["es2015", "stage-2", "stage-3"]
}

And we also need to require Babel in our Truffle configuration so it knows it should use it when compiling tests.

Note: Babel is an add-on for NodeJS which lets us use next-generation JavaScript in current-generation NodeJS, so we can write things like import etc. If this is beyond your understanding, simply ignore it and just paste this verbatim. You’ll probably never have to deal with this again after installing it this way.

require('dotenv').config();

================== ADD THESE TWO LINES ================
require('babel-register');
require('babel-polyfill');
=======================================================

const WalletProvider = require("truffle-wallet-provider");
const Wallet = require('ethereumjs-wallet');

// ...

Now, finally run truffle test. The output should be similar to this one:

A successful test

For more information about testing, see this tutorial, which we prepared specifically to cover testing of smart contracts.

In subsequent parts of this course, we’ll be skipping the tests, as typing them out would make the tutorials too long, but please refer to the final source code of the project to inspect them all. The process we just went through has set up the environment for testing, so you can just write the tests with zero further setup.

Whitelist

Let’s build the whitelisting mechanism now, which lets users participate in building the story. Add the following function skeletons to StoryDao.sol:

function whitelistAddress(address _add) public payable {
    // whitelist sender if enough money was sent
}

function() external payable {
    // if not whitelisted, whitelist if paid enough
    // if whitelisted, but X tokens at X price for amount
}

The unnamed function function() is called a fallback function, and that’s the function that gets called when money is sent to this contract without a specific instruction (i.e. without calling another function specifically). This lets people join the StoryDao by just sending Ether to the DAO and either getting whitelisted instantly, or buying tokens, depending on whether or not they are already whitelisted.

The whitelistSender function is there for whitelisting and can be called directly, but we’ll make sure the fallback function automatically calls it when it receives some ether if the sender has not yet been whitelisted. The whitelistAddress function is declared public because it should be callable from other contracts as well, and the fallback function is external because money will be going to this address only from external addresses. Contracts calling this contract can easily call required functions directly.

Let’s deal with the fallback function first.

function() external payable {

    if (!whitelist[msg.sender]) {
        whitelistAddress(msg.sender);
    } else {
        // buyTokens(msg.sender, msg.value);
    }
}

We check if the sender isn’t already on the whitelist, and delegate the call to the whitelistAddress function. Notice that we commented out our buyTokens function because we don’t yet have it.

Next, let’s handle the whitelisting.

function whitelistAddress(address _add) public payable {
    require(!whitelist[_add], "Candidate must not be whitelisted.");
    require(!blacklist[_add], "Candidate must not be blacklisted.");
    require(msg.value >= whitelistfee, "Sender must send enough ether to cover the whitelisting fee.");

    whitelist[_add] = true;
    whitelistedNumber++;
    emit Whitelisted(_add, true);

    if (msg.value > whitelistfee) {
        // buyTokens(_add, msg.value.sub(whitelistfee));
    }
}

Notice that this function accepts the address as a parameter and doesn’t extract it from the message (from the transaction). This has the added advantage of people being able to whitelist other people, if someone can’t afford to join the DAO for example.

We start the function with some sanity checks: the sender must not be whitelisted already or blacklisted (banned) and must have sent in enough to cover the fee. If these conditions are satisfactory, the address is added to the whitelist, the Whitelisted event is emitted, and, finally, if the amount of ether sent in is greater than the amount of ether needed to cover the whitelisting fee, the remainder is used to buy the tokens.

Note: we’re using sub instead of - to subtract, because that’s a SafeMath function for safe calculations.

Users can now get themselves or others whitelisted as long as they send 0.01 ether or more to the StoryDao contract.

Conclusion

We built the initial part of our DAO in this tutorial, but a lot more work remains. Stay tuned: in the next part we’ll deal with adding content to the story!

Frequently Asked Questions on Building Ethereum DApps and Whitelisting

What are the prerequisites for building Ethereum DApps?

Before you start building Ethereum DApps, you need to have a basic understanding of blockchain technology, Ethereum, and smart contracts. You should also be familiar with programming languages such as JavaScript and Solidity, which is used for writing smart contracts on Ethereum. Additionally, you need to install tools like Node.js, Truffle, Ganache, and MetaMask, which are essential for developing and testing DApps.

How does the whitelisting process work in DApps?

Whitelisting is a security measure used in DApps to restrict access to certain functions or areas of the application. It involves creating a list of approved addresses that are allowed to interact with the DApp. Only transactions initiated from these addresses will be accepted, while others will be rejected. This helps prevent unauthorized access and malicious activities.

What is the role of smart contracts in DApps?

Smart contracts are self-executing contracts with the terms of the agreement directly written into code. They play a crucial role in DApps as they automate the execution of business logic on the blockchain. They ensure transparency, security, and immutability, as once deployed, they cannot be altered or tampered with.

How can I test my DApp?

Testing is a crucial part of DApp development to ensure its functionality and security. You can use tools like Truffle and Ganache for testing. Truffle provides a development environment, testing framework, and asset pipeline for Ethereum, while Ganache allows you to create a private Ethereum blockchain for testing purposes.

What is the DAO and how is it related to DApps?

DAO stands for Decentralized Autonomous Organization. It’s a type of organization represented by rules encoded as a computer program that is transparent, controlled by the organization members and not influenced by a central government. A DAO’s financial transactions and rules are maintained on a blockchain which makes it a form of DApp.

How can I ensure the security of my DApp?

Ensuring the security of your DApp involves several practices. This includes writing secure smart contracts, thorough testing, conducting security audits, and keeping the software and dependencies up to date. It’s also important to follow best practices for secure coding and to stay informed about the latest security vulnerabilities and threats in the blockchain space.

What is MetaMask and why is it important in DApp development?

MetaMask is a browser extension that allows you to interact with the Ethereum blockchain and DApps directly from your browser. It also serves as an Ethereum wallet for managing your Ether and ERC-20 tokens. It’s important in DApp development as it provides a user-friendly interface for users to interact with your DApp without needing to run a full Ethereum node.

How can I deploy my DApp?

Once you’ve developed and tested your DApp, you can deploy it on the Ethereum mainnet or a testnet. This involves compiling your smart contracts, deploying them to the blockchain, and connecting your DApp to these contracts. You can use tools like Truffle and Infura for this process.

What are the challenges in DApp development?

DApp development comes with several challenges. This includes dealing with the scalability issues of the Ethereum network, ensuring the security of the DApp, managing the volatile gas prices for transactions, and providing a user-friendly interface. It also requires staying updated with the rapidly evolving blockchain technology and regulations.

How can I update my DApp after deployment?

Updating a DApp after deployment can be challenging as the smart contracts on the blockchain are immutable. However, you can design your contracts to be upgradable by separating the data and logic into different contracts, or using a delegate call to an upgradable contract. It’s important to plan for upgrades and changes in the design phase of your DApp.

Bruno SkvorcBruno Skvorc
View Author

Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app, the most advanced NFT system in the world, which allows NFTs to own other NFTs, NFTs to react to emotion, NFTs to be governed democratically, and NFTs to be multiple things at once.

DAODAppethereumethereum-hubethereum-tutorials
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week