DAOs and Web3

Go up to the CCC HW page (md) | view one-page version

Overview

In this assignment you are going to create a Decentralized Autonomous Organization (DAO). The DAO will issue NFT tokens (that you created in the Ethereum Tokens (md) assignment) to keep track of who are members, allow members to submit proposals, and allow voting on those proposals. For the purposes of this assignment, a proposal consists of a reason (which can be any string), an amount to pay, and an account address to pay it to. You will then make a Web3 interface for this DAO that will allow anybody with a standard web browser to see the status of the DAO and the proposals. The web page will only read from the blockchain using the web3.js library.

Beyond general experience with programming Solidity (which you have at this point it the course), this assignment requires:

In addition to your source code, you will submit an edited version of daoweb3.py (src).

NOTE: The Javascript code you develop herein does not work for all browsers. We know it works on Firefox and Chrome, and that it does not work fully on Safari. Note that you will need Chrome for the Metamask homework.

Changelog

Any changes to this page will be put here for easy reference. Typo fixes and minor clarifications are not listed here. So far there aren’t any significant changes to report.

Create the DAO

A Decentralized Autonomous Organization (DAO) allows members to submit and vote on proposals. For the purposes of this assignment, these proposals always have some amount of ether associated with them. A proposal is whether to pay the amount of ether to a particular account address, which is also specified in the proposal. One example of what to use a DAO for is a charity organization that focuses on one particular type of area (education, environment, etc.): members vote on what to donate money to (and how much to donate), and a successful proposal means that organization receives the amount of ether specified in the proposal.

As far as this assignment is concerned, we are not concerned as to what your DAO is for, only that it works.

Your contract must implement the provided IDAO interface below. Your contract opening line must be: contract DAO is IDAO {. This interface was adapted from the open-source code for The ÐAO, but was heavily modified to both work with current versions of Solidity and to fit better with this assignment. In particular, we removed a number of features, since this assignment is really about Web3; what was removed included splitting off a sub-DAO, including transaction data in the proposal, anything relating to quorum or quorum modifications, blocking members, vote freezes, etc. In a real DAO, these would need to be implemented as well.

The file is IDAO.sol (src). That file has many comments to explain how it works; those comments are not shown below.

// SPDX-License-Identifier: GPL-3.0-or-later

import "./IERC165.sol";

pragma solidity ^0.8.24;

interface IDAO is IERC165 {

    // A struct to hold all of our proposal data
    struct Proposal {
        address recipient;      // The address where the `amount` will go to if the proposal is accepted
        uint amount;            // The amount to transfer to `recipient` if the proposal is accepted.
        string description;     // The amount to transfer to `recipient` if the proposal is accepted.
        uint votingDeadline;    // A UNIX timestamp, denoting the end of the voting period
        bool open;              // True if the proposal's votes have yet to be counted, otherwise False
        bool proposalPassed;    // True if the votes have been counted, and the majority said yes
        uint yea;               // Number of Tokens in favor of the proposal; updated upon each yea vote
        uint nay;               // Number of Tokens opposed to the proposal; updated upon each nay vote
        address creator;        // Address of the shareholder who created the proposal
    }

    //------------------------------------------------------------
    // These are all just public variables; some of which are set in the
    // constructor and never changed

    function proposals(uint i) external view returns (address,uint,string memory,uint,bool,bool,uint,uint,address);
    function minProposalDebatePeriod() external view returns (uint);
    function tokens() external view returns (address);
    function purpose() external view returns (string memory);
    function votedYes(address a, uint pid) external view returns (bool);
    function votedNo(address a, uint pid) external view returns (bool);
    function numberOfProposals() external view returns (uint);
    function howToJoin() external view returns (string memory);
    function reservedEther() external view returns (uint);
    function curator() external view returns (address);

    //------------------------------------------------------------
    // Functions to implement

    receive() external payable;
    function newProposal(address recipient, uint amount, string memory description, 
                          uint debatingPeriod) external payable returns (uint);
    function vote(uint proposalID, bool supportsProposal) external;
    function closeProposal(uint proposalID) external;
    function isMember(address who) external view returns (bool);
    function addMember(address who) external;
    function requestMembership() external;

    // also supportsInterface() from IERC165

    // Events to emit

    event NewProposal(uint indexed proposalID, address indexed recipient, uint indexed amount, string description);
    event Voted(uint indexed proposalID, bool indexed position, address indexed voter);
    event ProposalClosed(uint indexed proposalID, bool indexed result);

}

This may look like a lot, but it’s not quite as much as it seems:

The files you will need are:

You will need the IDAO.abi file, which contains the ABI for the IDAO interface – you’ll need this when writing your Javascript code to interact with the interface.

You will also need your NFTManager.sol file from the Ethereum Tokens (md) assignment, and any other .sol files that are needed to allow that to compile (such as ERC721.sol, Strings.sol. Address.sol, etc.).

The requirements on this section are intentionally vague – the intent is to let you program your DAO any way you want. The only requirement is that your DAO must fulfill the spirit of the IDAO.sol (src) interface. As far as we are concerned, a proposal description primarily consists of a single string – perhaps it’s a URL, perhaps a statement, etc.

Don’t overthink this! The intent is just for you to get a working DAO. It doesn’t have to be perfect. In fact, this is the easier part of this homework, since we’ve all written a bunch of Solidity programs by now. The longest methods here are 8 lines.

DAO Notes

Solidity Notes

Unique URIs in NFTManager

Your NFTManager, from the Ethereum Tokens (md) assignment, should have required a unique URI for each NFT minted. For this assignment we don’t care what the URI is, only that they have a NFT. In particular, for this assignment, the URI does not need to resolve to an actual image. This also means that you don’t have to change the _baseURI() function from what you wrote in the previous assignment. For that matter, you don’t have to change anything in your NFTManager.sol file at all for this assignment (assuming you got it working).

However, you still have to have a unique URI for each one. One way to do this is to use their account address. The Strings.sol (src) file was used in the Ethereum Tokens (md) assignment (it was imported by ERC721.sol (src)). You will also need Math.sol (src) for Strings.sol to compile. Strings.sol has a toHexString() function that takes in an address as a parameter. This function will convert the address into an ASCII hex string with the leading ‘0x’. However, this hex string will be 42 characters long, and our NFTManager doesn’t allow more than 32 character strings. You can use the following substring() method to reduce it to a 32 character string:

function substring(string memory str, uint startIndex, uint endIndex) public pure returns (string memory) {
    bytes memory strBytes = bytes(str);
    bytes memory result = new bytes(endIndex-startIndex);
    for(uint i = startIndex; i < endIndex; i++)
        result[i-startIndex] = strBytes[i];
    return string(result);
}

You would call it such as:

string memory uri = substring(Strings.toHexString(addr),2,34);

Your URL

Everybody is going to have a different URL. To lessen the temptation, you have to put a (mostly) secret suffix as part of your URL.

We realize this part is going to be rather annoying, but we all have to do it.

To determine your URL:

As an example, consider one of the course accounts, 0xd0958cecfa9ee438c6c6b6e7a2295866065c4c59

When submitting the file, the auto-grader will check the suffix as well.

The following code, contained in get-url-ext.py (src) will compute this value as well. You have to pip install both web3 and pysha3 for this code to work.

# to run this, first: `pip install web3 pysha3`
import sys, web3, sha3

# sanity checks
assert len(sys.argv) == 2, "Must supply one parameter, the eth.coinbase account"
assert int(web3.__version__.split('.')[0]) >= 6, "You must install web3 version 6.0.0 or greater to run this code"

# compute and print out the suffix
checksummed = web3.Web3.to_checksum_address(sys.argv[1])
hash = sha3.keccak_256(bytes(checksummed,'ASCII'))
suffix = hash.hexdigest()[:8]
print("dao_"+suffix+".html")

You do not need to understand how that code works for this homework – you’ll be seeing all that code, and more, in the next homework.

Usage:

$ python3 get-url-ext.py 0xd0958cecfa9ee438c6c6b6e7a2295866065c4c59
dao_fe2a2995.html
$

Web3 Introduction

You are going to write a web interface that shows the status of your DAO and the proposals therein. This web page will only read from the blockchain.

There are a few strict requirements for this section: all of your code – both HTML and Javascript – must be in a single file called dao_XXXXXXXX.html. That file must be in your ~/public_html/ directory on your CS server account. This means that the URL for your page will be https://www.cs.virginia.edu/~mst3k/dao_XXXXXXXX.html, where mst3k is your userid.

This is not a class on user interfaces, so we are not expecting an amazing looking website – we are going to grade it on the functionality, not the appearance. That being said, it needs to be readable and navigable.

Setup

Preventing directory viewing

If you go to your home page on the departmental viewer, you can see all the files in the directory listing that shows up. To prevent this, we are going to create a default (and empty) index.html file. On the departmetnal server, you can just run touch ~/public_html/index.html.

NOTE: If you already have a web page present, or otherwise have prevented (intentional or not) directory viewing, then no further steps are needed.

Background

You are encouraged to look at the examples of web3 usage provided so far in class, and to copy/adapt the code therein. Note that you can copy from the materials provided by the course, NOT your classmates! The ones we have seen are:

The links to all of these are on the Canvas landing page.

You are welcome to look at the blockchain explorer code as well, but that won’t be as useful for this assignment – there is no web3 done by the explorer web page. When you go to each of the pages listed above (and, if necessary, enter a valid smart contract address), you can view the page source to see what is going on. Loading up the developer console make make it easier to view the code, and – later on – see any Javascript errors. Note that while these example URLs have an extension of .php, what you are viewing is still HTML and Javascript.

Javascript

async and await

Most function calls execute quickly. But some, such as those querying a blockchain, can take some time to return a value. In Javascript, these are called async functions. While async functions might execute quickly, Javascript assumes they will take some time. You have two options here – you can either tell Javascript to wait for the call to return, or give it a code block to execute whenever it does return. If you choose the first option, it will hang until that async call completes. If you choose the second option, it will move on, and later (in another thread) execute that code block when the async call does finally complete.

Consider the following Javascript code; this is from poll.php, which was used to display the Poll of your smart contract from the dApp Introduction (md) assignment.

var contractAddress = '0x01234567890abcdef01234567890abcdef012345';

let web3 = new Web3('URL');

abi = [...]; // the source has the full ABI, which is removed here for clarity

contract = new web3.eth.Contract(abi,contractAddress);

const getNumChoices = async() => {
    return await contract.methods.num_choices().call();
}

const getChoiceInfo = async(i) => {
    let x = await contract.methods.choices(i).call();
    return [x.name,x.votes];
}

const setChoiceInfo = async(i) => {
    getChoiceInfo(i).then(l => { document.getElementById("choice_"+i).innerHTML = l[0]; 
                                 document.getElementById("votes_"+i).innerHTML = l[1];
                                 // this next line is not thread-safe!!!
                                 document.getElementById("total_votes").innerHTML = parseInt(document.getElementById("total_votes").innerHTML) + parseInt(l[1]);
                                });
}

Four variables are defined:

You will notice that many of the fields and methods here are the same as in geth. Indeed, what we call web3.eth.blockNumber here is just eth.blockNumber in geth. They are also the same steps we’ve used before to call a function from geth – we saw this in the Debtors example, and those commands are listed in the Geth reference document (md).

We then define three async functions:

To call this code, we use the following:

function loadTable() {
    // The "main" part of this script -- once we know how many choices there are.
    document.getElementById("total_votes").innerHTML = "0";
    getNumChoices().then(val => {
        // set that value in the "Total number of choices:"  paragraph
        document.getElementById("num_choices").innerHTML = val;
        // create the table body to list the votes and choices
        text = "<tr><th>ID</th><th>Votes</th><th>Choice</th></tr>";
        // for each of the choices, create a separate table row with unique IDs
        for ( var i = 0; i < val; i++ )
            text += "<tr><td id='id_" + i + "'>" + i + "</td><td id='votes_" + i + "'></td><td id='choice_" + i + "'></td></tr>";
        // write the table to the HTML page; this must happen BEFORE we start filling in the votes and choices
        document.getElementById("choice_list").innerHTML = text;
        // call the async function that will fill in the number of votes and choices into the table
        for ( var i = 0; i < val; i++ )
            setChoiceInfo(i);
    });
    getPurpose().then(val => {
        // update the purpose field
        document.getElementById("purpose").innerHTML = val;
    });
}

loadTable();

We’ll get to document.getElementById() shortly, but it’s just updating part of the HTML page. You will notice that this calls getNumChoices(), an async function, and gives it a code block to execute when that function does return. Like Python, there is no main() function, so we have to call loadTable() to kick all this off.

As mentioned above, you are encouraged to use the code provided in the pages that the course has used so far. A number of Javascript functions therein will be of use in formatting your display – in particular, convertTimestamp(), short_hash(), and copy_link().

Debugging Javascript

Your web browser can load up the developer window (ctrl-shift-C, perhaps). This window has a Console tab, which is quite useful for debugging Javascript. You will likely have to reload the page after you have opened up the developer window. This console is just like the geth console – you can type Javascript commands into it, and access variables that were defined in the <script> sections of your webpage. In your Javascript code, you can use console.log() to print out values to this Javascript console. This is useful in debugging, and can be called from regular and async functions.

HTML and CSS

We assume you are familiar with the basics of HTML. If not, you can quickly come up to speed with an appropriate web search.

Below is some sample HTML code to start with:

<!DOCTYPE HTML>
<html lang="en">
    <head>
        <title>DAO Information</title>
        <meta charset="utf-8">
        <script src="web3.min.js"></script>
        <style></style>
    </head>
    <body style="margin-top:0">
        <h2>DAO Information</h2>
        <p>The DAO information is:</p>
        <p>Total proposals: <span id="total_proposals">loading...</span></p> <!-- line 12 -->
        <table id="dlist center"></table>
        <script>
// Javascript code here
        </script>
    </body>
</html>

A few notes:

Events

There are multiple ways to see when something happens on a blockchain, and they all involve listening for events that were emitted by the contracts (or the EVM itself).

Subscriptions

A web3 subscription is when the web3 library is listening for an event or multiple events. To do so, we specify the specific contract that we are listening to. Consider the following code:

function subscribeToPollEvents() {
    var options = { address: '0x01234567890abcdef01234567890abcdef012345' };
    var sub = web3.eth.subscribe('logs', options, function(err,event) {
        if ( !err )
            console.log("event error: "+event);
    });
    // pay attention to these subscription events:
    sub.on('data', event => loadTable() )
    sub.on('error', err => { throw err })
}

subscribeToChoiceEvents();

There are a number of things going on here:

There are a number of aspects of events that we are not covering in this assignment – in particular, one can listen for specific events, or specific events with specific parameters; see the first answer here for an example. One can also listen for the mining of a block – the dex.php code does this, as that is how it knows to update the (fake) ETH price (to see how, search for “newBlockHeaders” in the source code for dex.php). The dex.php also updates a single row when a transaction occurs.

Past Events

This part is not required at all for this assignment, but was included for completeness. Feel free to skip it if you are not interested.

You can find all the past events that a contract (or many contracts) emitted. The blockchain explorer uses this to find when an NFT was minted or transferred, or when a TokenCC is minted or transferred; both emit the Transfer() event (this is done in the ERC20.sol or ERC721.sol contract). In Javascript, you use the getPastLogs() function.

As an example, we will examine a NFTManager contract. Pick any deployed NFTManager, and get it’s address. Go to any of the .php pages that already load up the web3 Javascript library. Load up the Javascript console (ctrl-shift-C). Enter the following commands, putting in the appropriate value (the address of the NFTManager we are getting the events of):

var addr='0x01234567890abcdef01234567890abcdef012345'

You can then pull up the past logs via:

logs = await web3.eth.getPastLogs({fromBlock:0,toBlock:'latest',address:addr})

You can then examine the logs variable by just typing logs in the Javascript console. For the course-wide NFTManager from the tokens assignment, this is the result (you may see different values, but the concepts are the same):

The relevant fields are:

The logs variable is a list of these entries, and one can quickly go through them in a for loop to examine what events were emitted.

Lastly, this can be done in the geth terminal as well. However, the version of Javascript in geth is quite old, and doesn’t have the async functionality, so we have to use different syntax:

var addr="0x01234567890abcdef01234567890abcdef012345"
filter=web3.eth.filter({fromBlock:0,toBlock:'latest',address:addr})
eth.getLogs(filter.options)

The result, though, is (more or less) the same.

Web page

Your task is to create a dao_XXXXXXXX.html web page to display all the relevant information about the DAO’s proposals and balance. It should update every time an event is received; it can update the entire table rather than a specific row. A screen shot of the information we are looking for is below. Note that you should display this information, but the exact formatting is up to you. And if you want to display more, that’s fine too. A human is going to check this part of the assignment, so there is more leeway because it is not being auto-graded.

You don’t have to implement the copy link in the Creator column. Note that ALL of your HTML, CSS, and Javascript code must in the dao_XXXXXXXX.html file. The only thing that can be separate is the web3.js file, which is included in the HTML template above.

Once deployed, the DAO contract for your final submission should contain at least three proposals: one of which should have expired by the time the assignment is due AND ALSO called closeProposal() on it, and one which will stay open for one week after the assignment (just get the right date; we don’t care what time on that day). The third one is up to you.

As long as your web page starts the proposal IDs from 0, and increments them for each new proposal, you can view the deployed course-wide DAO to test your web page, as well as your own deployed DAO – just change the address in your dao_XXXXXXXX.html file. But be sure to change it back to your own DAO! Note that it needs to work on your DAO by the time you submit it! The address for the course-wide DAO is on the Canvas landing page.

Course DAO

You need to join the course-wide DAO and vote on one of the proposals. The address for the course-wide DAO is on the Canvas landing page. This DAO also follows the IDAO.sol (src) interface.

To join, you can just call requestMembership(), which will add you as a member. You then need to vote on one of the proposals. It doesn’t matter which proposal you vote on, or how you vote. Save the transaction hash from when you voted, as you will have to submit that.

Submission

You will need to fill in the various values from this assignment into the daoweb3.py (src) file. That file clearly indicates all the values that need to be filled in. That file, along with your Solidity source code, are the only files that must be submitted. The ‘sanity_checks’ dictionary is intended to be a checklist to ensure that you perform the various other aspects to ensure this assignment is fully submitted.

There are five forms of submission for this assignment; you must do all five.

Submission 1: You must deploy your DAO smart contract to our private Ethereum blockchain. It’s fine if you deploy it a few times to test it.

Submission 2: You need to have your dao_XXXXXXXX.html properly working at https://www.cs.virginia.edu/~mst3k/dao_XXXXXXXX.html, where mst3k is your userid. This means it needs to be in your ~/public_html directory on the departmental servers. You should have web.js (or web3.min.js) in that website directory as well. Needless to say, it should properly connect to your deployed DAO smart contract.

Submission 3: You need to add some data to your DAO contract, as specified above. In particular, that means at least three proposals (one of which has expired, one of which stays open for one week). Also make the specified Ethernet account address – indicated on the Canvas landing page – is a member of your DAO so that that address can perform tasks on your DAO to grade it.

Submission 4: You need to join the course-wide DAO and vote on one of the proposals.

Submission 5: You should submit your DAO.sol file, your dao_XXXXXXXX.html file, and your completed daoweb3.py file, and ONLY those three files, to Gradescope. All other imported files will be provided by Gradescope (including NFTManager.sol, if needed). NOTE: Gradescope cannot fully test this assignment, as it does not have access to the private blockchain. So it can only do a few sanity tests (correct files submitted, successful compilation, valid values in daoweb3.py, etc.).