Key Takeaways
- Story DAO utilizes OpenZeppelin’s Ownable contract to ensure that only the owner can execute administrative functions, enhancing security and control over the DApp’s operations.
- The Story DAO contract features adjustable parameters for fees and durations, with safeguards to prevent unauthorized changes, ensuring that only the owner can modify critical settings.
- Whitelisting within Story DAO is managed through a function that requires a payment, allowing for automated and conditional access based on the sender’s contribution.
- Comprehensive testing strategies, including both Solidity and JavaScript tests, are crucial for verifying the functionality and security of the Story DAO, ensuring robust operation before deployment.
- The deployment process for Story DAO is streamlined through Truffle, with specific migration scripts and configurations, facilitating smooth transition from development to live environment.
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:
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 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.