Skip to content

🛤️ Key Design Decisions

Janison Sivarajah edited this page Mar 2, 2023 · 3 revisions

Network

We had original started with Polygon but during the MVP build we decided to move to Ethereum Mainnet. This was done for better security that mainnet provides as well as direct integrations with other on-chain systems like ENS.

Basic Structure

The contract will contain a mapping that would serve as the main collection of apps. These apps or sites would be a struct and have its own schema and functionality to represent and in the future, automate infrastructure from on-chain contracts. We hope this structure in the future becomes an ERC standard for how to describe infrastructure on-chain. In addition to that, the contract will implement ERC721 where we would have a token for each site that is created in the sites database.

Furthermore, we will then add structures and functionality in the contracts that will allow community members to run copied or alternatives for apps minted on NFA. This will most likely be another struct to describe community hosting instances, and mappings required to store a collection of them and linking them back to the NFT token ID and main app or site. We would also extend the access control to authorize requests related to the community hosted version.

Factory and singleton vs Collection

At first, we were discussing whether or not the contracts should be singletons that are linked via some router or if we should make a single contract instance that will hold a collection of all the sites. The decision was to start a collection due to the following benefits:

  • upgradeability (via proxy)
  • simpler code
  • close integration with ERC721

Maybe in the future if we need to add meta-transactions we may think about going to a factory and router model where we would create a site contract for each site but that is yet to be needed.

Access Control

At the start, the assumption was to use the OpenZeppelin’s AccessControl module. But after some tests we have found that AccessControl would not fit our needs. Changing roles for tokens after a transfer transaction would not be possible because the contract can’t list the addresses bound to a role, so clearing the token role addresses would not be possible. To solve this issue we tried also using the OpenZeppelin’s AccessControlEnumerable module, but to clear up a role would require iterating and removing each of the addresses bound to it, which would be gas intensive and expensive for some cases. Another goal we wanted to accomplish with the roles management was to retrieve all the addresses bound to a role, which we would be unable simply extending EnumerableAccessControl.

We came up with the FleekAccessControl which fills the requirements mentioned above. With it, we are able to have role based control of each token and also for the entire collection individually. Our developed contract uses the combination of mapping and arrays to implement:

  • Address-role binding;
  • Enum based roles that can be extended;
  • A versioning system that doesn’t require iterations to clean up the roles.

How do we link tokens and Sites objects?

Both will be in separate mappings and links via the struct. The following was a quick example and differs from what's in our source but was the example used as a starting point:

   mapping(uint256 => uint256) public tokenIdToLevels;

   struct Site { 
      address owner;
      address controller;
      string contentId; //ipfs hash example)
      string ENS;
      string sourceCodeUrl; // maybe we break into another struct
   }

   uint[] SitesIndex;
   mapping(uint => Site) SitesMap;

   function geSitefromToken(uint256 tokenId) public view returns (site memory) {
    Site site = SitesMap[tokenId];
    return site;
   }

For this, we landed at using a mapping similar to above and using the same ID as the NFT for the sites database. We then use the properties on the struct to dynamically generate the URI based on the ERC721 (more on that in a later section).

Library

Should we create a library? If so when? https://www.geeksforgeeks.org/solidity-libraries/

This could be a consideration after POC once we have an initial codebase, to see if there are any libraries that we can package stateless, reused code into. But for now there was no library created.

Proxy

This can be used update the contracts functionality. This is something we are postponing for now because we can add a proxy in front of our sites contract at any point. And we can discuss upgradeability closer to deploying to mainnet. It is in our plan to make it upgradeable with a proxy, but it's not the current focus.

Token

Thanks to input from this Discord conversation: https://discord.com/channels/888907382816636939/888907383496118330/1045816944999276637, some of the tokenomics was discussed.

  • Allowlists - no allow lists for now. For now the contract owner will mint, but in the future we can see other parties having permission to mint.
  • Distribution - for the MVP we are not focusing on the minting cost, transfer fees, or supply. We would like to establish value for the underlying sites contracts and have further clarity on how this would be rolled out and integrated before we set those.
  • Blacklisting - We may also consider blacklisting in the future. We don't have any blacklistees ATM but would definitely like to have the mechanism to add any if we need to later for whatever reason.

Metadata

Traditionally art NFTs use a JSON file that has the content hash. We saw in the Alchemy docs an approach that was suitable for what we needed. Surfacing the metadata as a base64 string from the contract itself instead of pointing to an IPFS file

   function getTokenURI(uint256 tokenId) public returns (string memory){
    Site site = getSitefromToken(tokenId);
    bytes memory dataURI = abi.encodePacked(
        '{',
            '"owner": "Owner: ', site.owner, '",',
            '"controller": "Controller: ', site.controller, '",',
            '"contact": "Content: ', site.contentId, '",',
        '}'
    );
    return string(
        abi.encodePacked(
            "data:application/json;base64,",
            Base64.encode(dataURI)
        )
    );

This allowed us to surface the URI as a run time value based on the current value of the variables of our contracts, i.e., dynamic metadata.

Based on this we set our own metadata structure:

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        _requireMinted(tokenId);
        address owner = ownerOf(tokenId);
        App storage app = _apps[tokenId];

        bytes memory dataURI = abi.encodePacked(
            '{',
                '"name":"', app.name, '",',
                '"description":"', app.description, '",',
                '"owner":"', Strings.toHexString(uint160(owner), 20), '",',
                '"external_url":"', app.externalURL, '",',
                '"image":"', app.image, '",',
                '"attributes": [',
                    '{"trait_type": "ENS", "value":"', app.ENS,'"},',
                    '{"trait_type": "Commit Hash", "value":"', app.builds[app.currentBuild].commitHash,'"},',
                    '{"trait_type": "Repository", "value":"', app.builds[app.currentBuild].gitRepository,'"},',
                    '{"trait_type": "Version", "value":"', Strings.toString(app.currentBuild),'"}',
                ']',
            '}'
        );

        return string(abi.encodePacked(_baseURI(), Base64.encode((dataURI))));
    }