How to build your first DataDAO Factory on FVM

How to build your first DataDAO Factory on FVM

This tutorial will enable you to program the Filecoin deal market and create multiple FVM Data DAOs.

What is FVM

Let's start by understanding what is FVM. FVM stands for Filecoin Virtual machine. As a web3 developer, you may already know that filecoin is a decentralized storage provider in most layman's terms (or a peer-to-peer network that stores files, with built-in economic incentives to ensure files are stored reliably over time, to be exact). Now think of adding a computational layer on the top of filecoin, that FVM for you.

The FVM unlocks boundless possibilities, ranging from programmable storage primitives (such as storage bounties, auctions, and more), to cross-chain interoperability bridges (e.g. trustlessly connecting Filecoin with Ethereum, Solana, NEAR, and more), to data-centric Decentralized Autonomous Organizations (DAOs), to Layer 2 solutions (such as reputation systems, data availability sampling, computation fabrics, and incentive-aligned Content Delivery Networks), and more.

What is Storage Deal

Before moving ahead, let us get a quick insight into how the cycle between storage provider, storage, client, and marketplace revolves.

Storage deals refer to the stored data that is picked up by a Storage Provider in the Filecoin network. A deal is initiated with the Storage Client to store the data. The deal’s details and metadata of the stored data are then uploaded onto the Filecoin blockchain. FVM/FEVM allows interaction with this metadata, effectively computing over the state.

Building Data DAO contract

This tutorial would be based on this repo.

Now, let's write smart contracts and understand the logic inside them.

DataDAO.sol

This contract enables the storage provider to add the CID to store data with the Filecoin built-in deal market. The process of adding the CID has to go through a policy check where the CID needs to be approved by DAO members via a voting mechanism.

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.13;

// import {StdStorage} from "../lib/forge-std/src/Components.sol";
import {specific_authenticate_message_params_parse, specific_deal_proposal_cbor_parse} from "./CBORParse.sol";

contract MockMarket {

    DataDAO client;

    constructor(address _client) {
        client = DataDAO(_client);
    }

    function publish_deal(bytes calldata raw_auth_params, uint256 proposalID) public {
        // calls standard filecoin receiver on message authentication api method number
        client.handle_filecoin_method(0, 2643134072, raw_auth_params, proposalID);
    }

}

contract DataDAO {
    uint64 constant public AUTHORIZE_MESSAGE_METHOD_NUM = 2643134072; 
    // number of proposals currently in DAO
    uint256 public proposalCount;
    // mapping to check whether the cid is set for voting 
    mapping(bytes => bool) public cidSet;
    // storing the size of the cid
    mapping(bytes => uint) public cidSizes;

    mapping(bytes => mapping(bytes => bool)) public cidProviders;

    // address of the owner of DataDAO
    address public immutable owner;

    struct Proposal {
        uint256 proposalID;
        address storageProvider;
        bytes cidraw;
        uint size;
        uint256 upVoteCount;
        uint256 downVoteCount;
        uint256 proposedAt;
        uint256 proposalExpireAt;
    }

    // mapping to keep track of proposals
     mapping(uint256 => Proposal) public proposals;

    // mapping array to track whether the user has voted for the proposal
    mapping(address => mapping(uint256 => bool)) public hasVotedForProposal;

/**
 * @dev constructor: to set the owner address
 */
constructor(address _owner) {
     require(_owner != address(0), "invalid owner!");
     owner = _owner;
}

/***
 * @dev function to create new proposal
 */
    function createCIDProposal(bytes calldata cidraw, uint size) public {
        proposalCount++;
        Proposal memory proposal = Proposal(proposalCount, msg.sender, cidraw, size, 0, 0, block.timestamp, block.timestamp + 1 hours);
        proposals[proposalCount] = proposal;
        cidSet[cidraw] = true;
        cidSizes[cidraw] = size;
    }

    /**
     * @dev function to vote in favour of proposal
     */
    function upvoteCIDProposal(uint256 proposalID) public {
        require(!isCallerSP(proposalID), "Storage Provider cannot vote his own proposal");
        require(!hasVotedForProposal[msg.sender][proposalID], "Already Voted");
        require(isVotingOn(proposalID), "Voting Period Finished");
        proposals[proposalID].upVoteCount = proposals[proposalID].upVoteCount + 1;
        hasVotedForProposal[msg.sender][proposalID] = true;
    }

    /**
     * @dev function to vote in favour of proposal
     */
    function downvoteCIDProposal(uint256 proposalID) public {
        require(!isCallerSP(proposalID), "Storage Provider cannot vote his own proposal");
        require(!hasVotedForProposal[msg.sender][proposalID], "Already Voted");
        require(isVotingOn(proposalID), "Voting Period Finished");
        proposals[proposalID].downVoteCount = proposals[proposalID].downVoteCount + 1;
        hasVotedForProposal[msg.sender][proposalID] = true;
    }

    /**
     * @dev function to check whether the policy is accepted or not
     */
    function policyOK(uint256 proposalID) public view returns (bool) {
        require(proposals[proposalID].proposalExpireAt > block.timestamp, "Voting in On");
        return proposals[proposalID].upVoteCount > proposals[proposalID].downVoteCount;
    }

    /**
     * @dev function to authorizedata and store on filecoin
     */
    function authorizeData(uint256 proposalID, bytes calldata cidraw, bytes calldata provider, uint size) public {
        require(cidSet[cidraw], "CID must be added before authorizing");
        require(cidSizes[cidraw] == size, "Data size must match expected");
        require(policyOK(proposalID), "Deal failed policy check: Was the CID proposal Passed?");
        cidProviders[cidraw][provider] = true;
    }

    /**
     * @dev function to handle filecoin
     */
    function handle_filecoin_method(uint64, uint64 method, bytes calldata params, uint256 proposalID) public {
        // dispatch methods
        if (method == AUTHORIZE_MESSAGE_METHOD_NUM) {
            bytes calldata deal_proposal_cbor_bytes = specific_authenticate_message_params_parse(params);
            (bytes calldata cidraw, bytes calldata provider, uint size) = specific_deal_proposal_cbor_parse(deal_proposal_cbor_bytes);
            cidraw = bytes(bytes(cidraw));
            authorizeData(proposalID, cidraw, provider, size);
        } else {
            revert("The Filecoin method that was called is not handled");
        }
    }

    // getter function also used in require statement

    /**
     * @dev function to get the storage provider address
     */
     function getSP(uint256 proposalID) view public returns(address) {
        return proposals[proposalID].storageProvider;
    }

    /**
     * @dev function to check whether the function caller is the storage provider
     */
    function isCallerSP(uint256 proposalID) view public returns(bool) {
       return getSP(proposalID) == msg.sender;
    }

    /**
     * @dev function to check whether users can start voting on the proposal
     */
    function isVotingOn(uint256 proposalID) view public returns(bool) {
       return proposals[proposalID].proposalExpireAt > block.timestamp;
    }

    /**
     * @dev get the address of this contract
     */
    function getAddressOfContract() public view returns (address) {
        return address(this);
    }
}

Let’s now go step by step. The storage provider can create a proposal to add his CID using createCIDProposal function.

As FEVM is pre-launch to Filecoin’s mainnet, FEVM actors as of today, cannot yet interact easily with storage deals on the Filecoin network.

To simulate this for the hack, we have sample CID-related data. These deals are built into the Wallaby test network and your actor can interact with them.

Sample Test Data

testCID = "0x000181E2039220206B86B273FF34FCE19D6B804EFF5A3F5747ADA4EAA22F1D49C01E52DDB7875B4B";
testSize = 2048
testProvider = "0x0066";
testmessageAuthParams = "0x8240584c8bd82a5828000181e2039220206b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b190800f4420068420066656c6162656c0a1a0008ca0a42000a42000a42000a";

DataDaoFactory.sol

Now let's write the Factory contract to interact with this DataDAO smart contract and create multiple versions of it according to our needs.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "./DataDAO.sol";

contract DataDoaFactory{
    // factory contract owner
    address public immutable dataDaoFactoryOwner;

    // number of DataDAO created
    uint256 public numOfDataDao;

    // struct to store all the data of dataDao and dataDaoFactory contract
    struct dataDaoFactoryStruct {
        address dataDaoOwner;
        address dataDaoFactoryOwner;
    }

    // searching the struct data of DataDao and DataDoaFactory using owner address
    mapping(address => dataDaoFactoryStruct) public allDataDaos;

    // owner address will be used check which address own/create a new dataDAO
    // mapping(ownerAddress => smart contract address)
    mapping(address => address) public searchByAddress;

    /**
     * @dev constructor to get the owner address of this contract factory
     */
    constructor(address _dataDaoFactoryOwner) {
        dataDaoFactoryOwner = _dataDaoFactoryOwner;
    }

    /**
     * @dev function to create the contract DATADAO
     */
    function createDataDao(address _dataDaoOwner) public {
        DataDAO dataDao = new DataDAO(
            _dataDaoOwner
        );
        // Increment the number of DataDao
        numOfDataDao++;

        // Add the new DataDAO to the mapping
        allDataDaos[_dataDaoOwner] = (
            dataDaoFactoryStruct(
                _dataDaoOwner, // address of dataDAO owner 
                address(this)
            )
        );

        // search the profile by using owner address
       searchByAddress[_dataDaoOwner].push(address(dataDao));
    }



    // get the balance of the contract
    function getContractBalance() public view returns (uint256) {
        return address(this).balance;
    }

    // get the address of this contract
    function getAddressOfContract() public view returns (address) {
        return address(this);
    }

     // function to withdraw the fund from contract factory
    function withdraw(uint256 amount) external payable {
        require(msg.sender == dataDaoFactoryOwner, "ONLY_ONWER_CAN_CALL_FUNCTION");
        // sending money to contract owner
        require(address(this).balance >= amount, "not_enough_funds");
        (bool success, ) = dataDaoFactoryOwner.call{value: amount}("");
        require(success, "TRANSFER_FAILED");
    }

    // get the address of DataDaoFactory contract owner
    function getAddressOfDataDaoFactoryOwner() public view returns (address) {
        return dataDaoFactoryOwner;
    }

    // receive function is used to receive Ether when msg.data is empty
    receive() external payable {}

    // Fallback function is used to receive Ether when msg.data is NOT empty
    fallback() external payable {}
}

Now compile the DataDaoFactory smart contract and deploy it on Wallaby- Testnet for Filecoin.

Now let's create a new Data DAO using our DataDaoFactory contract.

Let us create an add CID proposal, using the above test data.

Once you have created the proposal, you would be able to see all the basic data related to that proposal

Now, it's time for the DAO members to vote on that proposal before the voting period ends. If the total count of upvotes is greater than the total count of downvotes then the proposal would be passed else it would be considered rejected. This contract uses a simple DAO mechanism, you can frame and implement your own rules and make it more interesting and secure.

Note: Only members apart from the storage provider would be able to cast a vote and each member can vote only once.

Enough said. It’s time to vote! To keep it simple, I would be upvoting and passing this proposal. You can always play around with this.

If you go back and check the details of the proposal the count of upvotes would have been incremented to 1 whereas the downvotes would be still zero.

Note: This voting process should be completed within 1 hour from the creation of the proposal. You can make changes to the contract and increase or decrease the timer accordingly.

Okay. All done with the voting part !!

Now that we have passed the proposal, now it's time to publish our deal. In case the DAO failed the proposal, the storage provider won’t be able to publish his deal.

We have a mock marketplace contract, let's deploy that first. This contract takes the contract address of our DAO contract i.e the client contract inside its constructor.

Let us publish the deal, this function takes two inputs the message auth param which you can get from the test data, and the respective proposal ID.

If the proposal was passed, this function would get executed successfully else, it would fail.

That’s it! This was a quick guide as to how you can play around with the basic client contract.

This project is made by an amazing team of Harsh Ghodkar, Suvraneel Bhuin and Aayush Gupta

Reference

https://medium.com/@rvk_rishikesh/build-your-first-datadao-on-fvm-6ed38b940103

🎉BOOM 🎉

You have completed the whole tutorial. Give yourself a pat on the back. You have learned about:

  • Filecoin Virtual Machine (FVM)

  • Build your own DataDAO smart contract

  • Build DataDaoFactory contract to build multiple versions of DataDAO.

  • Publish a deal using MockMarket Contract

💥 Simply WOW 💥

If you learn and enjoy this article. Please share this article with your friends. I hope you learned something new or maybe even solved a problem. Thanks for reading, and have fun!

You can follow me on Twitter, GitHub, and LinkedIn. Keep your suggestions/comments coming!

WAGMI 🚀🚀

Did you find this article valuable?

Support Aayush Gupta by becoming a sponsor. Any amount is appreciated!