Best Practices for Building Blockchain Solutions with Solidity - Part I

in cryptocurrency •  6 years ago 

()

Overview

The purpose of this article is to consolidate findings around design and development best practices in the Solidity environment. While blockchain technologies are still evolving, it is implicitly understood that some of the best practices outlined in this article are subject to change and subject to debate. As the market continues to evolve, it is imperative that discussions around governance and standards of excellence happen on a continual basis. As such, feedback is highly welcome. Guidelines that are specifically geared towards building Dapps, in many cases, leverage and build upon software best practices that have existed in the Information Technology industry for decades.

This article intends to be a starting point, or at least one of the starting points, for a conversation around Solidity best practices on the Linkedin forum. Furthermore, this article intends to be the first in a series of articles that subsequently cover advanced topics via collaboration with other blockchain professionals.

Scope

The content here is geared towards Ethereum Solidity. This article will touch upon recommendations for nomenclature, account management, user interface development, error handling, security, inheritance, performance and scalability, and robustness. Upward migration and deployment using truffle are out of scope for this installment. The intended audience is blockchain enthusiasts. The Solidity snippets illustrated herein are compatible with the 0.4.23 parser and the Javascript snippets use ES6 syntax. Please note that for the sake of conciseness, the snippets are fragmented.

I) Nomenclature for Solidity Contracts

  1. Smart contract names are typically nouns with the starting letter in upper case and the rest in lower.

  2. The above bullet applies to struct declarations as well since they describe abstract entities.

  3. All variable declarations should be in lower case.

II) Account Management

  1. Passwords and private keys should be saved as soon as they are generated. The password will be needed when unlocking an account.

  2. In order to safeguard your password, accounts should be unlocked in interactive mode as opposed to typing the password at the command line where it might be captured in the console history. The danger of exposing the password on the screen is evident in b) or using --password parameter in a). Choose a) over b).

a) geth --unlock <account_address> By executing this line, you will be prompted for the password (for the reasons explained above, it is not advisable to use the --password parameter in this command).

b) web3.eth.personal.unlockAccount(<account_address>, < pass_word >)

  1. The 12-word mnemonic (seed-phrase) generated by Metamask should be stored somewhere safe. In the event private keys are lost or misplaced, the mnemonic can be used to regenerate the existing set of accounts.

III) User Interfaces

Decentralized solutions need special considerations when it comes to developing intuitive and interactive end-user applications. End users do not interact with a smart contract directly but with the front-end app that provides an interface between the user and the Dapp. For illustration purposes, it is assumed that UI is built with Javascript.

  1. Present Users with Confirmation dialogs informing them with the irreversibility of transactions.
    Blockchain transactions are architected for immutability in a P2P distributed environment. The end user needs to be aware that they cannot create a support ticket with a central authority that can reverse the unintended transaction like they did with traditional apps. As such, it's best practice to prompt the end user with a confirmation dialog with the salient features of the transaction especially all the financial attributes involved, giving the user the option to scrutinize the values one more time so they can choose to proceed or cancel.

  2. Consider the channels used for consuming the Dapp; mobile, web or both.

  3. Provision UI widgets that account for latency.
    While transaction latency largely depends on the consensus algorithm, it is best practice to provision widgets such as button spinners or Progress views that let the end user know that the transaction may not be processed instantaneously given the nature of the blockchain. This is especially true of Proof-Of-Work consensus algorithms as used by Solidity currently.

  4. Rendering of UI for mobile clients with slow connections.
    Server-side rendering might alleviate some of the frustration of an end user's Dapp experience from a cell phone interface on a slow connection. In these scenarios, using a utility such as Next.js can create some initial HTML content on the screen while the javascript code is rendered by the react utility inside the browser. On the flip side, if the browser (client) has to do all the work, that might leave users with an empty screen for a while until rendering completes.

  5. UI widget and page visibility
    Since the blockchain relies on users to perform transactions based on assigned authority, UI design should follow suit by presenting the appropriate functionality to authorized users. In the design phase, it needs to be determined which pages/screens different users have access to, what widgets they can see and what actions they can perform.

  6. Ensure that the render() method accounts for any changes to the state of the contract.
    By doing so, you can ensure that new values appear on the screen without a manual refresh.

  7. Wrap transaction calls in try-catch blocks.
    All calls to the smart contract from the front-end app should be wrapped in try-catch blocks so that a user-friendly error is returned to the message on the UI component such as a form. This is covered further in the next section.

IV) Error Handling

  1. Use try-catch blocks.
    As covered in the earlier section on UI, try-catch blocks should be used when invoking smart contract methods from the application and error messages should be displayed on the UI. This is cleaner and easier than doing explicit input validations, etc.

  2. Capture error messages.
    In the event of an error, ensure that the UI component (such a form) re-renders by capturing the value of the error object in a state variable.

class IndexPage extends Component {

    state = {errorMsg: ''} 

     onClick = async () => { 

     try { 

        //invoke a call to the smart contract 

     } 

    catch (err) { 

          this.setState({errorMsg: err.message});

}

…..

}

render() {

/*Use a react or semantic-ui-react component like Message to return the value of state variable errorMsg to the user. When using a Form, be sure to add an error property to the Form. */

}

}

The above is preferable to Metamask returning a cryptic error in the console but no user-friendly message to the user in a contained widget.

V) Security
The smart contract needs to ensure that functions that modify the state of the blockchain (transactions) can only be invoked by the authorized parties.

Create reusable modifiers to qualify functions instead of implementing checks at the code-level inside the function (in order to keep the snippets concise, the constructor function that assigns the msg.sender attribute to the manager variable will not be written below. Please assume that it exists in the contract). Given the following options, choose a) over b).

a) contract Game {

   address manager; 

           modifier restricted() { 

                   require(msg.sender == manager); 

                    _;               (the underscore servers as a placeholder for code substitution ) 

                   } 

            function pickWinner (uint i) public restricted returns (address) { 

          ….. 

          ….. 

     } 

}

b) contract Game {

    address manager; 

           function pickWinner (uint i) public returns (address) { 

             require(msg.sender == manager); //(where manager is a wallet address of                                                                                            the manager of the contract) 

               …... 

                      ….. 

           } 

  } 

In a), we have defined a reusable modifier that can be applied to multiple functions, that substitutes the underscore with the function code after checking for AuthZ. In a), the function can focus on its core logic instead of checking for Auth issues.

VI) Inheritance

Reusability across multiple contracts should be considered in the design phase. It's very likely that there will be other smart contracts in the future that will require a manager. In that event, the previous section could be rewritten in the following way (as mentioned in the previous section, please assume that the constructor exists).

contract Manageable{

address manager;

modifier restricted() {

    require(msg.sender == manager); 

        _;        // (the underscore serves as a placeholder for code substitution from the function) 

     } 

}

import "./Manageable.sol"; //for the sake of simplicity for this article, assume it's in the same folder

contract Game is Manageable{

function pickWinner(uint i) public restricted returns (address) { 

            ….. 

}

}

This method and notation eliminates redundant declarations of the manager variable across multiple contracts and also makes the restricted modifier reusable with a two-fold benefit.

  1. Functions can focus on the business/transaction related code and not have to keep up with AuthZ changes.

  2. Core AuthZ functionality can be managed in one spot, which might reduce the amount of regression testing.

VII) Performance/Scalability and Gas Consumption

  1. While blockchain algorithms are evolving in order to alleviate gas charges to the user, the deployment of smart contract code entails gas, as does any transaction that alters data/state on the blockchain. Computationally intensive functions that require high processing power and high bandwidth cost more than those that require less. The following recommendations can not only decrease said costs but also create more readable, maintainable and efficient code.

  2. Where possible, replace array traversals that use loops with mapping lookups.

  3. Arrays are suitable for collections where all that is needed for a lookup is an index.

  4. When using uint for variable declarations, if the maximum size is identified and set in stone during design-time, a precision in the maximum number of digits is recommended in the declaration, for instance, uint16, uint32, etc.

  5. Examine code for the possibility of infinite loops by checking if terminal conditions are met in all scenarios.

  6. If a multi-part expression is going to be referenced multiple times, consider saving it to a variable and referencing the variable instead within if statements, etc.

  7. Infrastructure auto-scalability should be provisioned either horizontally or vertically (depending on what is appropriate) for mining nodes when anticipating peak loads.

VIII) Robustness

In addition to implementing all of the above practices towards building robust code, the safest way to ensure robustness is through creating comprehensive, repeatable and automated test cases.

Node.js provides various test frameworks such as mocha, chai, and jest that allow you to interact with smart contracts.

  1. Put Remix to Use
    Before starting to write test code using any of the frameworks mentioned above, the use of Remix (https://remix.ethereum.org/) is highly recommended for testing the smart contract. Remix is a browser-based on-the-fly development and testing platform. It allows access to 10 accounts with 10 pseudo ether each inside an embedded JavaScript VM that makes testing contract calls fast and convenient.

  2. Using test frameworks
    Create test cases for every use case that the contract covers. Specifically, every financial transaction should be accounted for.

  3. Verify balances
    Sender and receiver wallets should be validated against the exact expected debit and credit amounts as well as gas consumption where appropriate. The contract balance should be validated regularly against expected amounts. Beginning and ending balances should be verified for accuracy.

  4. Ensure all test cases pass before UI design/development and upstream migration.
    Design and thoroughly test the contract before UI development. Making changes to the smart contract and UI code after UI development could entail substantial work and is therefore not recommended. All test suites should be executed successfully before deploying to testnet or mainnet or any network that requires real currency. Test cases should be validated using assertions.

  5. Plan for performance testing
    System peak loads should be anticipated based on the SLAs and should be simulated prior to testnet deployments.

  6. Consider async nature of transactions.
    As mentioned in the UI section, calls to web3 from javascript should be wrapped in try-catch blocks, Within the try block, smart contract function calls should be wrapped with async-await due to the asynchronous nature of calls to web3, which serves as the bridge between Javascript and the Ethereum network. In the snippet below, the ES6 syntax allows the use of async/await instead of promises and also provides a cleaner and compact look.

    onClick = async () => { 
    
           const accounts = await web3.eth.getAccounts();   /*assume web3 module was imported 
    
          …. 
    
     }
    

Conclusion

As covered at the beginning of the document, these are still early days of a revolutionary technology that is currently very much in flux. It is therefore incumbent upon all of us to evaluate architectural and development practices on a regular basis and contribute our findings to the community. This document is not meant to be viewed as a Bible for development but rather as a starting point for a conversation.

Any blockchain solution's ultimate goal should be to enhance and enrich the end user's experience. There is no substitution for communication with end users so that they are proactively informed with how their experiences with blockchain solutions will deviate from that of traditional centralized solutions, which might have spanned decades. Any suggestions/corrections related to the content of this document are absolutely welcome.

Image credits:

  1. istockphoto.com - Person looking at Blockchain Concept on Screen, Standard license
Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Congratulations @rajita! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Do not miss the last post from @steemitboard:

SteemitBoard - Witness Update
Vote for @Steemitboard as a witness to get one more award and increased upvotes!