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.
Contents:
- Application licensing
- Traditional approach to licensing
- Licensing based on the distributed database
- Getting the license
- Activating the license
- Checking if the license is active
- How to implement licensing using smart contracts
- Preparing a smart contract
- Adding security and permissions
- Testing the contract
- Interaction with smart contracts from the application
- Transactions vs. calls
- Authentication for sending transactions
- Conclusion
- Resources
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.
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.
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.
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.
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.
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.
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:
- Solidity and the Ethereum platform
- Truffle framework for compilation and local testing
- Ethereum Wallet to deploy contracts to the Rinkeby network
For the target application thatโs licensed:
- cURL
- JSON RPC
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:
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);
}
}
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.
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.
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:
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.
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.
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!