How to Build Your First Data DAO Factory on the FVM
This tutorial will enable you to program the Filecoin deal market and create multiple Data DAOs on the Filecoin Virtual Machine.
What is the Filecoin Virtual Machine?
Let's start by understanding the Filecoin Virtual Machine (FVM). As a Web3 developer, you may already know that Filecoin is a decentralized storage provider 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.
The FVM runs Rust code compiled to WASM, but many smart contract developers know Solidity. That's why a Filecoin node also comes with a Solidity interpreter on top of the FVM called the Filecoin Ethereum Virtual Machine (FEVM)
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 a storage provider in the Filecoin network picks up. 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 is based on this repository.
Now, let's write smart contracts and understand the logic inside them.
DataDAO.sol
This contract enables the storage provider to add the content identifier (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 through it 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 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 according to our needs.
"`solidity // 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[msg.sender] = ( dataDaoFactoryStruct( msg.sender, // address of dataDAO owner address(this) ) );
// search the profile by using owner address searchByAddress[msg.sender] = 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 will be able to see all the essential 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 count of downvotes, the proposal will pass; otherwise, it will reject. This contract uses a simple DAO mechanism; you can frame and implement your rules and make them more secure.
Note: Only members apart from the storage provider can 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 proposal details, the count of upvotes would have been incremented to 1
, whereas the downvotes would still be zero.
Note: This voting process will be complete within 1 hour of the proposal's creation. You can change the contract and increase or decrease the timer accordingly.
Okay. All done with the voting part!!
Now that we have passed the proposal, it's time to publish our deal. In case the DAO fails 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 passed, this function would get executed successfully; else, it would fail.
That's it!
This was a quick guide on how to play around with the basic client contract.
This project was created by a fantastic 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 the following:
- 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 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 ๐๐
Subscribe to my newsletter
Read articles from Aayush Gupta directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Aayush Gupta
Aayush Gupta
Actively looking for Smart Contract Developer, DevRel and Technical Writer Role. ex Smart Contract Developer @Lighthouse | Technical Writer | @QuickNode Ambassador | @chainlink Developer Expert & Community Advocate | Contributor @Developer_DAO, @LearnWeb3DAO and @eden Built over 70 dapps and published 19 tutorials in web3