Logo
blank Skip to main content

How to Develop an Ethereum Smart Contract for Licensing?

The Apriorit team always tries to stay on top of the latest industry trends. While blockchain technology is at the peak of popularity, our developers are diving deep into the creation of blockchain-based applications. In this article, we want to share our experience in developing smart contracts for application licensing via blockchain.

In addition to cryptocurrency, the blockchain is used for security, smart contracts, and record keeping. In this article, we describe how the blockchain can be applied to build a licensing system for an application. We decided to use Ethereum as the decentralized platform for creating our smart contract. A smart contract, which is an application, runs exactly as itโ€™s written without any possibility of changing it. Using smart contracts for blockchain software licensing allows tracking the ownership of the license and moving its value around. Weโ€™ll go through the whole development process step by step and finally analyze the result.

Application licensing

Traditional approach to licensing

The traditional approach to licensing involves a web service and some central database server that stores information about licenses and users. When an application first starts, the user has to activate the license somehow (either by entering a login and password or some generated key) and then the application contacts the server to verify the license.

Itโ€™s obvious that a problem can arise in case something happens to the database that stores all the information. Of course, the database can be replicated and then easily restored, but things can get worse if an attacker modifies some data.

So letโ€™s see what we get if we replace the central database with decentralized storage.

Read also:
Application Licensing with Blockchain: EOS Network

Licensing based on a distributed database

This approach differs in that the information about all issued licenses is stored in the form of blocks in a blockchain. Since the database is decentralized, itโ€™s not so easy to harm the data, which is one of the key benefits of DeFi. Using a blockchain also eliminates the risk of data tampering, as a blockchain canโ€™t be modified.

The whole process of getting and activating licenses should generally look the same for the end user. Though the internal architecture differs due to integration with a decentralized ledger, all the additional steps for interacting with the blockchain are performed behind the scenes.

Traditional Licensing vs Distributed

The interaction with the distributed database is performed through the interface of a smart contract. An entity token or license token is used to represent the instance of the license.

We can also consider another architecture option that eliminates the necessity for an intermediate web service and is based on direct communication between the application and the blockchain network.

Blockchain Licensing Process

The benefit of such a solution is that the license data is saved directly to the distributed ledger. Therefore, fewer components handle the data and work with the blockchain.

However, this approach assumes that the user has some Ether on their account in order to purchase the license. The application also needs to have credentials of the account owner in order to send transactions to the blockchain.

Weโ€™ll focus on the first approach involving a web service and a distributed ledger. Now letโ€™s analyze the licensing process and its integration with the blockchain.

Read also:
How NFTs can Help Businesses: Use Cases, Benefits, and Nuances to Consider

Getting the license

A blockchain-based license is generally purchased on the product website. After purchasing a license, the user receives an activation key. This key is a unique ID of the license token. The license token is transferred to the userโ€™s Ethereum account.

So to prove that the user has purchased a license, their Ethereum account should contain one token on its balance.

Mint and Transfer of Entity Token in Smart Contract

Activating the license

In order to activate a license, the user enters the activation key into the application. Under the hood, the application sends a transaction to the blockchain to mark this license as active using the unique identifier of the transaction. If the transaction succeeds, the license is activated. During activation, the token is bound to the unique identifier of the device in order to prevent the same license from being used on several devices. Moreover, according to the smart contractโ€™s logic, the token can be activated just once to prevent reusing the same token several times.

Token Activation in Smart Contract

Checking if the license is active

After a license has been activated, the application needs to periodically verify that the license is still valid and hasnโ€™t expired. If the license has already expired, the token is marked as expired and is no longer valid.

How to implement licensing using smart contracts

Now letโ€™s dive into the technical details of how to implement such a smart contract. Before we start, letโ€™s specify what technologies are used.

For smart contracts:

For the target application thatโ€™s licensed:

  • cURL
  • JSON RPC

Related services

Blockchain-based Solution Development

Preparing a smart contract

Weโ€™ll need a smart contract for creating and managing the token entity. Since the license token should be non-fungible, weโ€™re going to create our contract in compliance with the ERC-721 standard.

The LicenseToken smart contract holds a list of tokens together with information about the user account balance. Each token is an instance of the LicenseInfo structure, which includes the following information:

  • licenseType โ€” type of the license
  • registeredOn โ€” date and time when the token was created
  • expiresOn โ€” date and time when the token expires (this field is set during token activation)
  • state โ€” state of the license (inactive, active, or expired)
  • deviceId โ€” unique identifier of the device (e.g. computer) to which the license is bound

The LicenseToken smart contract implements the required functions of the ERC-721 standard, namely:

  • totalSupply
  • balanceOf
  • ownerOf
  • approve
  • transferFrom
  • safeTransferFrom
  • setApprovalForAll
  • getApproved
  • isApprovedForAll

We didnโ€™t implement several functions because their functionality is beyond the licensing logic.

Read also:
Building a Blockchain-Based Voting System: Opportunities, Challenges, and What to Pay Attention To

In addition to the functions mandatory for ERC-721, the contract also provides several methods that contain licensing business logic:

  • giveLicense โ€” creates a new token and transfers it to the account of the user who purchased the license
  • activate โ€” updates the token to mark it as activated (also sets the expiresOn field to track the lifetime of the license)
  • isLicenseActive โ€” checks the current state of the license token for the given token ID
  • handleExpiredLicense โ€” updates the state of the license token in case isLicenseActive has detected that the license is expired

The whole token contract looks as follows:

Solidity
pragma solidity ^0.8.7;
 
contract owned {
  address owner;
   
  constructor() {
    owner = msg.sender;
  }
   
  modifier onlyOwner {
    require(msg.sender == owner);
    _;
  }
   
  function transferOwnership(address newOwner) onlyOwner  public {
    owner = newOwner;
  }
}
   
contract LicenseToken is owned {
  enum LicenseType {WIN, MAC}
  enum LicenseState {ACTIVE, INACTIVE, EXPIRED}
   
  uint constant LICENSE_LIFE_TIME = 30 days;
   
  struct LicenseInfo {
    LicenseType licenseType;
    uint registeredOn;
    uint expiresOn;
    LicenseState state;
    string deviceId;
  }
   
  LicenseInfo[] tokens;
   
  mapping (uint256 => address) public tokenIndexToOwner;
  mapping (address => uint256) ownershipTokenCount;
  mapping (uint256 => address) public tokenIndexToApproved;
   
  event LicenseGiven(address account, uint256 tokenId);
  event Transfer(address from, address to, uint256 tokenId);
  event Approval(address owner, address approved, uint256 tokenId);
   
  constructor()  {
  }
   
  // ERC-721 functions
  function totalSupply() public view returns (uint256 total) {
    return tokens.length;
  }
   
  function balanceOf(address _account) public view returns (uint256 balance) {
     return ownershipTokenCount[_account];
  }
   
  function ownerOf(uint256 _tokenId) public view returns (address owner) {
    owner = tokenIndexToOwner[_tokenId];
    require(owner != address(0));
   
    return owner;
  }
   
  function transferFrom(address _from, address _to, uint256 _tokenId) onlyOwner public {
    require(_to != address(0));
    require(_to != address(this));
    require(_owns(_from, _tokenId));
   
    _transfer(_from, _to, _tokenId);
  }
   
  function approve(address _to, uint256 _tokenId) public {
    require(_owns(msg.sender, _tokenId));
    tokenIndexToApproved[_tokenId] = _to;
    emit Approval(tokenIndexToOwner[_tokenId], tokenIndexToApproved[_tokenId], _tokenId);
  }
   
  function safeTransferFrom(address _from, address _to, uint256 _tokenId) public {
    // method is not implemented because it is not needed for licensing logic
  }
   
  function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) public {
    // method is not implemented because it is not needed for licensing logic
  }
   
  function setApprovalForAll(address _operator, bool _approved) public  pure{
    // method is not implemented because it is not needed for licensing logic
  }
   
  function getApproved(uint256 _tokenId) public pure returns (address) {
    // method is not implemented because it is not needed for licensing logic
    return address(0);
  }
   
  function isApprovedForAll(address _owner, address _operator) public pure returns (bool) {
    // method is not implemented because it is not needed for licensing logic
    return false;
  }
   
  // licensing logic
  function giveLicense(address _account, uint _type) onlyOwner public {
    uint256 tokenId = _mint(_account, _type);
    emit LicenseGiven(_account, tokenId);
  }
   
  function activate(uint _tokenId, string memory _deviceId) onlyOwner public {
    LicenseInfo storage token = tokens[_tokenId];
    require(token.registeredOn != 0);
    require(token.state == LicenseState.INACTIVE);
   
    token.state = LicenseState.ACTIVE;
    token.expiresOn = block.timestamp + LICENSE_LIFE_TIME;
    token.deviceId = _deviceId;
  }
   
  function burn(address _account, uint _tokenId) onlyOwner public {
    require(tokenIndexToOwner[_tokenId] == _account);
   
    ownershipTokenCount[_account]--;
    delete tokenIndexToOwner[_tokenId];
    delete tokens[_tokenId];
    delete tokenIndexToApproved[_tokenId];
  }
   
  function isLicenseActive(address _account, uint256 _tokenId) public view returns (uint state){
    require(tokenIndexToOwner[_tokenId] == _account);
   
    LicenseInfo memory token = tokens[_tokenId];
    if (token.expiresOn < block.timestamp && token.state == LicenseState.ACTIVE) {
       return uint(LicenseState.EXPIRED);
    }
   
    return uint(token.state);
  }
   
  function handleExpiredLicense(address _account, uint256 _tokenId) onlyOwner public {
    require(tokenIndexToOwner[_tokenId] == _account);
   
    LicenseInfo storage token = tokens[_tokenId];
    if (token.expiresOn < block.timestamp && token.state == LicenseState.ACTIVE) {
       burn(_account, _tokenId);
    }
  }
   
  // internal methods
  function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
    return tokenIndexToOwner[_tokenId] == _claimant;
  }
   
  function _mint(address _account, uint _type) onlyOwner internal returns (uint256 tokenId) {
    // create new token
    LicenseInfo memory token = LicenseInfo({
        licenseType: LicenseType(_type),
        state: LicenseState.INACTIVE,
        registeredOn: block.timestamp,
        expiresOn: 0,
        deviceId: ""
    });
    tokens.push(token);
    uint id= tokens.length - 1;
    _transfer(address(0), _account, id);
    return id;
  }
   
  function _transfer(address _from, address _to, uint256 _tokenId) internal {
   ownershipTokenCount[_to]++;
   tokenIndexToOwner[_tokenId] = _to;
   
   if (_from != address(0)) {
     ownershipTokenCount[_from]--;
     delete tokenIndexToApproved[_tokenId];
   }
   emit Transfer(_from, _to, _tokenId);
  }
}

Read also:
5 Security Tips for Writing Smart Contracts

Adding security and permissions

Unlimited access to application licenses is one of the common smart contract vulnerabilities. Since smart contracts are deployed to a decentralized system, itโ€™s crucial to take measures to secure them and limit permissions.

We need to be sure that only the owner of the smart contract can:

  • transfer tokens;
  • give licenses;
  • activate licenses.

The owner of the contract can be the account from which the contract has been deployed to the network.

In the code of a smart contract, we can access the address (account) that called the contract method. This can be done with the help of the global variable msg.sender. The concept is to save this address in the constructor of the contract and require certain methods to confirm that the current caller is the owner. As a result, all attempts to execute methods from an account other than the ownerโ€™s will fail.

Solidity
address owner;
  
// in constructor
owner = msg.sender;
  
// defining the modifier
modifier onlyOwner {
        require(msg.sender == owner);
        _;
}
  
// applying for the function
function transfer(address receiver, unit amount) onlyOwner public {
      _transfer(owner, receiver, amount);
}

Thereโ€™s something like a template contract from the Ethereum documentation โ€” owned โ€” that provides this functionality and can be used as the base contract.

Related services

Security Testing

Testing the contract

Since a smart contract should be deployed to the blockchain network to run, debugging and testing is a little bit complicated. Nevertheless, in order to check that our smart contract works as expected, we can use different blockchain testing tools and methods. For example, we can test our smart contract with auto tests using the Truffle framework. Moreover, Truffle provides ten test accounts with some Ether on them that can be used for testing.

Hereโ€™s the JavaScript test on the basic flow:

Solidity
var LicenseToken = artifacts.require("./LicenseToken.sol");
  
contract('LicenseManagementTests', function (accounts) {
  it("test basic flow", async function() {
      let token = await LicenseToken.new({from: accounts[0]});
      let result = await token.giveLicense(accounts[1], 1, {from: accounts[0]});
      let tokenId = -1;
  
      // check transaction log for TokenMinted event in order to obtain the tokenId
      for (var i = 0; i < result.logs.length; i++) {
        var log = result.logs[i];
        if (log.event == "LicenseGiven") {
           // We found the event!
  
           tokenId = log.args.tokenId.valueOf();
           break;
          }
      }
  
      let balance = await token.balanceOf(accounts[1]);
      assert.equal(balance, 1, "User has 1 token after getting license");
  
      let isActive = await token.isLicenseActive.call(accounts[1], tokenId);
      // 1 - LicenseType.INACTIVE
      assert.equal(isActive, 1, "License is not active.");
  
      await token.activate(tokenId, "UDID");
      isActive = await token.isLicenseActive.call(accounts[1], tokenId);
      // 0 - LicenseType.ACTIVE
      assert.equal(isActive, 0, "License is active.");
  });
});

Interacting with smart contracts from the application

To make the application able to interact with a distributed database, we should have already deployed our smart contracts to the blockchain network. Since we need to activate the license and check whether itโ€™s active within the target application, itโ€™s necessary to have a way to interact with the contacts.

Ethereum provides the JSON RPC API, which can be used to call smart contracts. The Infura API can be used too. Itโ€™s like a wrapper over the JSON RPC API that provides additional scalability and security.

JSON RPC can be called either directly using cURL or using some wrapper libraries depending on the language and technology.

We wonโ€™t dive deep into the details of using JSON RPC, which can be found in the Ethereum documentation and may differ depending on the platform used by the target application. Instead, weโ€™ll focus on the main concepts of practical use of this API.

Read also:
Capturing Suspicious Transactions on the Ethereum Blockchain

Transactions vs. calls

Generally, JSON RPC provides two ways of calling smart contract methods: via sending transactions or via calls.

Hereโ€™s a small comparison of these methods:

Transactions

  • Transactions can be sent using the eth_sendTransaction or eth_sendRawTransaction method.
  • Each transaction consumes gas (the Ethereum fee for operation execution).
  • Each transaction results in creating a new block in the chain and therefore changes the state of the contract data (e.g. it modifies the value of the internal variables).
  • It takes some time to confirm each transaction.
  • Sending a transaction requires signing or unlocking the account.

Calls

  • Calls can be sent using eth_call.
  • A call doesnโ€™t consume gas.
  • A call doesnโ€™t produce a new block, and as a result doesnโ€™t change the state of the data (e.g. if you transfer some amount of tokens from one account to another using eth_call, the balance of both accounts will remain the same).
  • Calls are executed almost immediately since they donโ€™t require any confirmation.

As you can see, there are significant differences in the use of transactions and calls that should be taken into consideration.

In the case of application licensing, we can go with the following scheme:

  • Methods like giveLicense, activate, and handleExpiredLicense should be definitely executed as transactions since they involve token transfers.
  • The isLicenseActive method can be executed as a call since it just verifies the license state.

Taking into account this scheme, we should also consider the time necessary to process and confirm transactions for the application UI/UX.

Authentication for sending transactions

Transactions should be signed prior to sending to the blockchain, and there are a couple of ways to achieve this:

  • A transaction can be signed with the private key on the application side and then sent via the eth_sendRawTransaction method.
  • The account from which the transactions are sent can be unlocked with the pass code using the personal.unlockAccount method. After that, the transaction can be sent via the eth_sendTransaction method without any prior signing.

By โ€œthe accountโ€ we mean the account of the contract owner whoโ€™s allowed to execute the contractโ€™s methods.

For authentication needs, the application has to keep either the private key or the password inside it, which is a potential security hole. As an alternative, the application can send requests to the web service that will then send transactions to the blockchain. So itโ€™s necessary to take additional measures to secure the user authentication.

Case Study:
Smart Contract Security Audit: Penetration Testing and Static Analysis

Conclusion

Weโ€™ve analyzed the approach to building application licensing on the basis of blockchain technology. We think that the blockchain can be considered an alternative to the traditional licensing approach since it provides the following advantages:

  • Data is protected against modification and tampering.
  • Data is stored decentrally, so damage to the central database server isnโ€™t an issue. As with any approach, though, it also has some issues:
  • Smart contracts should be properly designed and secured to eliminate the possibility of attacks.
  • Interaction with a blockchain from the application can impact the UX due to additional time for transaction confirmation.

Which approach to use depends on application specifics and licensing system requirements.

We hope this article was helpful for developers who want to leverage blockchain technology for their needs. The Apriorit team has expertise in developing blockchain-based applications for various purposes. Contact us if youโ€™re interested in adding various types of smart contracts to your software!

Resources

http://solidity.readthedocs.io/en/v0.4.24/

https://truffleframework.com/docs
https://github.com/ethereum/wiki/wiki/JSON-RPC
https://www.ethereum.org/token

http://erc721.org/

Have a question?

Ask our expert!

Tell us about your project

Send us a request for proposal! Weโ€™ll get back to you with details and estimations.

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us