Using Solidity with Ruby on Rails: Best Practices for Building Smart Contracts

Written by

In this article we are going to dive into the basic setup and best practices for using Ruby on Rails alongside Solidity to create a dApp. Here we tackle the installation of Ruby, Ruby on Rails, Ethereum developers’ tools, like Ganache and Truffle, the implementation of the necessary ruby gems to connect to an Ethereum network, and the best practices to integrate both technologies.

Let’s talk bases ⚾

Over the last few years, decentralized applications and web pages have gained immense popularity, and with very good reason. The advent of blockchain technology has revolutionized the way people interact, trade, and share information online, offering unparalleled security, transparency, and trust. Decentralized applications, or dApps, leverage the power of blockchain networks like Ethereum, enabling users to interact directly with one another without the need for central authorities or intermediaries. This has led to the emergence of innovative solutions across various industries, such as decentralized finance (DeFi), supply chain management, gaming, content-sharing platforms, and, our topic today, Smart Contracts. As we continue to witness the rapid growth and adoption of dApps, it becomes increasingly clear that they have the potential to reshape the digital landscape and redefine the way we conduct business and interact online.

On the other hand, Ruby on Rails (RoR) is one of the most popular frameworks nowadays. Writing web applications has become a simple, well-documented process with RoR. With a thriving community behind it and new gems (libraries) being developed every day, it stands out as one of the best options for web development.

Let’s gear up first 🤖

Before diving into the best practices, let’s review in a nutshell the process of setting up a web application in RoR that will use the Ethereum blockchain.

We won't delve too deeply into the specifics of the installations since there are a ton of tutorials and resources available on the installation topic and setup of Ruby and Ethereum-based apps. However, here are the basics:

Installing the main components

Ruby on Rails

Installing Ruby on Rails is quite a straightforward process, but in keeping with one of the core principles of RoR, DRY (Don't Repeat Yourself), we recommend referring to a comprehensive tutorial. The team at GoRails has created an easy-to-follow guide for installing Rails (and Ruby) on any system: Install Ruby On Rails.

P.S., Ruby only runs on MacOS and Linux natively; so, in order to install it on Windows, you must have WSL enabled and a Linux image installed. The tutorial covers these steps as well.

Set up Ethereum development tools

In order to interact with the Ethereum blockchain, we will need two tools: Ganache, which will allow us to simulate a personal Ethereum blockchain for development. And Truffle, a very useful Framework that will allow us to write our contracts.

To use both these tools, you need to have NodeJs installed in your machine. If you followed the GoRails tutorial, you should be good to go. If, for some reason, you want to install NodeJs individually, you can simply download it from the official web (Download Node.js) and install as any other software.

Ganache comes in 2 flavors, UI and command-line. To install the UI version, the process is as simple as going to Ganache - Truffle Suite, selecting your OS versions, and installing normally.

This way, you will have a very nice control center from which you can create and administer your workspaces.

If you need to install the command-line version (to implement on a server, for example), you can use this guide in the README here.

Ganache has recently launched an interactive Documentation page. Here, you can learn and test everything you need to create your SmartContract app.

To install Truffle (to compile and deploy your contracts), you can simply do the following:

npm install -g truffle

After this, you can simply install the Ethereum gem in your Ruby project. Just go to the Gemfile and add gem ethereum.rb then, run bundle install, and that should be it!

Creating smart contracts

First things first, we need to create a new workspace, we can use this command for that:

ganache-cli -i 5777 --mnemonic "your twelve-word mnemonic phrase here"

Now, let’s create a simple contract with Solidity for storing and retrieving a string value, called SimpleStorage.sol

pragma solidity ^0.8.0;

contract SimpleStorage {
    string private data;

    function set(string calldata _data) public {
        data = _data;
    }

    function get() public view returns (string memory) {
        return data;
    }
}

In order to use this contract in Rails, we need the ABI address.

Compile your contract

Initialize Truffle project:

Create a new directory for your Truffle project and navigate to it:

mkdir my_truffle_project
cd my_truffle_project

Initialize a new Truffle project:

truffle init

This command will generate the necessary Truffle project files and directories.

Add your Solidity contract:

Copy your SimpleStorage.sol file into the contracts directory within the Truffle project.

Configure Truffle:

Edit the truffle-config.js file to configure the networks you want to deploy to. For example, to connect to your local Ganache instance, add the following configuration:

const HDWalletProvider = require('@truffle/hdwallet-provider');
const mnemonic = 'your twelve-word mnemonic phrase here';

module.exports = {
  networks: {
    development: {
      provider: () => new HDWalletProvider(mnemonic, '<http://localhost:8545>'),
      network_id: 5777, // Match the network ID used when starting Ganache
      gas: 5500000,
      confirmations: 2,
      timeoutBlocks: 200,
      skipDryRun: true,
    },
  },

  compilers: {
    solc: {
      version: '0.8.0', // Match the Solidity version in your contract
    },
  },
};

Replace your twelve-word mnemonic phrase here with the same mnemonic you used when starting Ganache.

Note: You need to install @truffle/hdwallet-provider to use HDWalletProvider in the configuration:

npm install @truffle/hdwallet-provider

Create a migration script:

Create a new file named 2_deploy_simple_storage.js in the migrations directory with the following content:

const SimpleStorage = artifacts.require('SimpleStorage');

module.exports = function (deployer) {
  deployer.deploy(SimpleStorage);
};
r

Compile the contract:

In your Truffle project directory, run the following command to compile your Solidity contract:

truffle compile

This will generate the contract's ABI (Application Binary Interface) and Bytecode in the build/contracts directory.

Deploy the contract:

In your Truffle project directory, run the following command to deploy your contract to the specified network (in this case, the development network):

truffle migrate --network development

After the deployment is successful, you will see the contract's address in the console output, and you’ll be able to use it in your Rails app.

Connecting the contract with Rails

The first thing we need is to connect our Rails app to our Ganache workspace. Doing this is as simple as requiring the Ethereum gem and calling.

require 'ethereum'

ETHEREUM_CLIENT = Ethereum::HttpClient.new('<http://localhost:8545>')

Then, we need to create a model class for our contract, using our address and ABI from the compilation before.

class SimpleStorage
  CONTRACT_ADDRESS = '0x1234567890123456789012345678901234567890' # Replace this with your deployed contract address
  ABI = [...] # Replace this with your contract ABI

  def self.contract
    @contract ||= Ethereum::Contract.create(client: ETHEREUM_CLIENT, address: CONTRACT_ADDRESS, abi: ABI)
  end

  def self.set(data)
    contract.transact.set(data)
  end

  def self.get
    contract.call.get
  end
end

And that’s it. Your solidity contract is now usable inside our Rails application.

Now, let’s review what is the best way to implement this, and the Best Practices for Building Smart Contracts!

Practice makes you better, but best practices make you the best! 🌍

Writing Modular Code

One of the most important principles in any programming language is DRY (Don’t repeat yourself). One way to do this is by implementing a modular approach. This will allow us to use components we ourselves have created or implement libraries to achieve the best solution.

Another significant advantage of using modular code in smart contract development is that it allows for more straightforward testing and maintainability. As each module can be tested independently, it becomes easier to identify issues and verify the correct functionality of the individual components. Furthermore, modular code simplifies maintenance efforts since changes or bug fixes in one specific module do not necessarily impact the rest of the modules.

To enhance the modularity of smart contracts in Solidity, we can use the import statement to include external contracts or libraries. This functionality enables the creation of reusable components that can be imported and used across various contracts, further promoting modular code and ensuring that smart contracts remain efficient, maintainable, and easy to test. By adopting modular code and separation of concerns in smart contract development, we can create more robust and reliable decentralized applications (dApps) that leverage the full potential of blockchain technology.

Test-driven Development (TDD)

Adopting a Test-driven Development (TDD) approach is crucial for building reliable and secure smart contracts, and any application for that matter. TDD emphasizes writing tests before implementing the contract code, allowing us to focus on specifying desired behaviors and expected outcomes. The Truffle framework makes this process very easy by providing the necessary tools to write comprehensive tests in JavaScript or Solidity for Ethereum-based smart contracts.

The primary benefits of TDD in smart contract development include ensuring the contract behaves as expected and catching potential vulnerabilities before deployment. This is particularly important in blockchain technology, where smart contracts handle valuable assets and sensitive transactions. TDD also encourages modular and maintainable code since each component is developed and tested independently.

Embracing a Test-driven Development approach when building smart contracts leads to more reliable, secure, and maintainable contracts. With the Truffle framework tools and writing thorough tests, we can create dependable smart contracts, minimizing the risk of vulnerabilities and fostering trust in decentralized applications (dApps).

Adopting the MVC Architecture

Ruby on Rails, like many other frameworks, implements the Model-View-Controller (MVC) architecture; this incredible schema, when integrating Solidity with Ruby on Rails, ensures a clean separation of presentation, business, and data access logic. The MVC architecture is a design pattern that promotes modularity and maintainability in software applications, making it an ideal choice for combining Solidity.

Within the MVC architecture, Solidity contracts act as models, representing the data structures and business logic of the application. Rails controllers serve as the intermediary layer, responsible for interacting with the Ethereum blockchain and managing the data flow between Solidity contracts and views. And the Views do what they do best, display the information retrieved from the blockchain, presenting data to users in a coherent and visually appealing manner.

Adopting the MVC architecture when integrating Solidity with Ruby on Rails fosters a well-organized and maintainable application structure. By creating Solidity contracts as models, utilizing Rails controllers for blockchain interactions, and developing views for data presentation, we can build efficient and scalable decentralized applications that seamlessly blend the smart contract functionality with the robustness of the Ruby on Rails framework.

Securing Your Smart Contracts

The significance of security in smart contract development cannot be overstated, as vulnerabilities can result in substantial financial losses and damage to reputations. The idea behind smart contracts is basically an immutable, trackable, trustable contact. To ensure the security of your smart contracts, consider the following practices:

  1. Don’t reinvent the wheel and try these libraries: Existing open-source libraries, such as OpenZeppelin, helps mitigate potential security risks by incorporating battle-tested, audited code. These libraries often provide solutions to common smart contract vulnerabilities and help developers write secure, high-quality code.
  2. Incorporate access control: Implementing access control is essential to limit the functions that can modify the contract's state to specific roles or addresses. By restricting access, you can reduce the potential attack surface and maintain control over sensitive contract operations.
  3. Avoid using tx.origin: Relying on tx.origin can expose your contract to attacks, as it refers to the original initiator of the transaction rather than the current message sender. Instead, use msg.sender to obtain the caller's address, which provides a more secure and reliable way of identifying the sender of a transaction.
  4. Implement proper error handling: Employing appropriate error handling using require, assert, and revert statements is crucial for ensuring predictable contract behavior in the event of exceptions. Proper error handling helps prevent unexpected outcomes and provides developers with valuable information about the contract's execution.

Where to store?

In Solidity, there are two primary types of data storage: storage and memory. Understanding when and where to use each type is important for optimizing your smart contracts.

  1. Storage:
  • Use when: You want to store data persistently on the blockchain.
  • Where: Mainly used for contract state variables.
  • Cost: Reading and writing to storage are more expensive in terms of gas because it requires interacting with the Ethereum blockchain to persist the data.
  1. Memory:
  • Use when: You need to store temporary data within a function.
  • Where: Mainly used for local variables within functions.
  • Cost: Reading and writing to memory are cheaper in terms of gas because it is temporary and does not require interaction with the blockchain.

Here are some guidelines for using storage and memory:

  • If you need to store data that must persist across function calls and/or transactions, use storage. For example, contract balances, token ownership, and other contract state variables are typically stored in storage.
  • If you only need to store data temporarily within a function, use memory. This is useful for function arguments, return values, and temporary variables that do not need to be stored on the blockchain.
  • Remember that data from storage can be read into memory and vice versa. For example, if you have a state variable in storage and you want to work with it temporarily within a function, you can read the data into a memory variable, manipulate it, and then write it back to the storage if needed.
  • Be mindful of gas costs. Writing to storage is more expensive than writing to memory. Minimizing writes to storage can help reduce the gas costs of your smart contract.

By choosing the right data storage type for your use case, you can optimize performance and cost efficiency of your smart contracts.

Gas Optimization

Gas optimization is critical for minimizing the cost of deploying and interacting with your smart contracts. Here are some tips for optimizing gas usage:

a. Use view and pure functions: These functions do not modify the state of the contract and do not consume gas when called externally.

b. Minimize storage use: Storing data on the Ethereum blockchain is expensive. Reduce storage costs by using events, which are logged in the transaction receipts and do not consume as much gas as storage operations.

c. Use efficient data types: Use the most appropriate data type for your variables, as it can affect the gas usage. For example, uint8 is more gas-efficient than uint256 when storing small numbers.

d. Optimize loops: Loops can consume significant gas, especially if they iterate over large data sets. Consider using techniques like pagination or mapping to minimize gas consumption.

Something important to keep in mind, and this is more a must than a recommendation. When creating smart contracts, the functions we create inside these contracts must have a deterministic complexity, not a probabilistic one. So, random for loops or calls to an API, functions that we don’t know how much time they’ll take (how much gas they’ll consume) can not be created inside contracts.

Interaction between Ruby on Rails and Smart Contracts

To facilitate communication between Rails applications and smart contracts, follow these best practices:

a. Use the Ethereum.rb gem: This gem provides a simple interface to interact with the Ethereum blockchain, allowing you to call contract functions and deploy new contracts.

b. Use asynchronous calls: Blockchain transactions can take time to be mined and confirmed. Implement asynchronous calls in your Rails application using background jobs (e.g., Sidekiq or Delayed Job) to avoid blocking the main thread and ensure a responsive user experience.

c. Cache contract data: To minimize the number of calls made to the Ethereum network, cache frequently accessed contract data in your Rails application. This can be achieved using Rails caching mechanisms, such as Redis or Memcached.

d. Handle exceptions gracefully: Be prepared to handle exceptions that may occur when interacting with the Ethereum network, such as timeouts, rejections, or out-of-gas errors. Implement retries and fallback mechanisms in your Rails application to handle these scenarios.

Wrapping up

Integrating Solidity with Ruby on Rails for building smart contracts presents a potent synergy, allowing us to harness the robustness of the Rails framework and the versatility of Solidity. This powerful combination enables the creation of sophisticated decentralized applications (dApps) that leverage the strengths of both technologies, providing a seamless user experience and enhancing the overall functionality of the blockchain ecosystem.

By adhering to these best practices, we can build secure, scalable, and efficient decentralized applications that are not only reliable but also resilient to potential vulnerabilities. Implementing solid programming techniques such as Test-driven Development (TDD), Model-View-Controller (MVC) architecture, and prioritizing security measures help ensure the smooth operation of the dApps and lay a strong foundation for long-term success.

Furthermore, following these practices not only benefits us, developers, but also, protects the interests of users and stakeholders involved in the blockchain ecosystem. As smart contracts often handle valuable assets and sensitive transactions, ensuring their security and reliability is of the utmost importance. The effective implementation of these practices contributes to building trust and confidence in technology, and us as developers or companies.

Frequently Asked Questions