DEV Community

Cover image for Create a blockchain asset tracking system using an ERC-721-inspired smart contract!
Rafael Abuawad
Rafael Abuawad

Posted on

Create a blockchain asset tracking system using an ERC-721-inspired smart contract!

Introduction

Most asset tracking done today is done using something as basic and as insecure as a simple spreadsheet, in some cases a most sophisticated approach is done with some kind of database with custom software as an interface.

Both are extremely insecure and can be altered by an attacker. Let us imagine a company that distributes high-end computer boards, not only sells them but leases them to different types of organizations for testing, marketing, and more. If an attacker wanted to modify this register using the current approach it would be as simple as accessing the database or spreadsheet and changing a simple record.

Using multi-ownable smart contracts would prevent this kind of behavior by default, and asset tracking with NFTs can give better control of where is each asset and with some extra code give a lot more information than what can be stored in a simple spreadsheet.

Solving this problem

This problem can be solved using blockchain technology:

  1. Create smart contracts that enable the company to track its assets transparently and reliably.
  2. Create a custom blockchain to solve this issue.

in this article, we are going to see the first approach, but the second one can be as, if not more interesting and reliable.

Smart-contracts

For this approach, we only need one smart contract for tracking the assets.

We are going to use a similar approach to the ERC-721 token standard, but with a lot of parts removed and a lot of modifications.

Since we don't want users to trade tokens like a normal NFT, we also what the ability to remove token access to a given access, and we want to control who can have what.

Interfaces

This interface defines the asset collection itself and is a stripped-down version of an ERC-721.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

interface IAssetCollection {
    event Transfer(
        address indexed from,
        address indexed to,
        uint256 indexed tokenId
    );

    function name() external view returns (string memory);

    function balanceOf(address owner) external view returns (uint256);

    function tokensOf(address tokenHolder)
        external
        view
        returns (uint256[] memory);

    function ownerOf(uint256 tokenId) external view returns (address owner);

    function tokenURI(uint256 tokenId) external view returns (string memory);

    function mint(address tokenHolder, string memory tokenURI) external;

    function burn(address tokenHolder, uint256 tokenId) external;

    function transfer(uint256 tokenId) external;

    function transferFrom(
        address tokenHolder,
        address to,
        uint256 tokenId
    ) external;
}
Enter fullscreen mode Exit fullscreen mode

This interface describes is to allow control over the smart contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

interface IOwnable {
    event TransferOwnership(address indexed from, address indexed to);

    function owner() external view returns (address);

    function transferOwnership(address _newOwner) external;
}
Enter fullscreen mode Exit fullscreen mode

Smart-contract

The main smart contract is the Asset Collection smart contract, this contract is responsible for managing all the assets inside a particular collection, we (as the owners of the smart contract) can mint, burn, move and transfer tokens, each token represents an asset.

We don't want users to trade tokens (like NFTs), but we need the basic functionality from an ERC-721.

Due to limitations on smart contract data storage, we are storing all the relevant information outside the blockchain, preferably on a persistent storage platform (like Machina or Arweave), and using the token URI metadata pattern used commonly for NFTs.

Based on our work on the interface defined above, we will overview some functions.

Mint
The "mint" function can only be called ourselves (the owner) and is similar to the mint function found on any other NFT smart contracts. It gives us the ability to create a token.

function mint(address tokenHolder, string memory _tokenURI)
    public
    onlyOwner
{
    uint256 tokenId = _tokenIds;
    _tokenIds += 1;
    _setTokenURI(tokenId, _tokenURI);
    _mint(tokenHolder, tokenId);
}
Enter fullscreen mode Exit fullscreen mode

Burn
The "burn" method allows us to destroy a token permanently.

function burn(address tokenHolder, uint256 tokenId)
    public
    onlyOwner
{
    _mint(tokenHolder, tokenId);
}
Enter fullscreen mode Exit fullscreen mode

Transfer
The "transfer" method returns a token back to us, and can only be called by the token owner itself.

function transfer(uint256 tokenId) public {
    _transfer(msg.sender, _owner, tokenId);
}
Enter fullscreen mode Exit fullscreen mode

Transfer from
The "transfer from" method allows us (the owner) to control tokens on behalf of a user. If we want to change the holder or if we need to take it back and assign it to ourselves again.

// Moves a token from one user to another
function transferFrom(
    address tokenHolder,
    address to,
    uint256 tokenId
) public onlyOwner {
    _transfer(tokenHolder, to, tokenId);
}
Enter fullscreen mode Exit fullscreen mode

The final smart contract should look something like this.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "./interfaces/IAssetCollection.sol";
import "./interfaces/IOwnable.sol";

contract AssetCollection is IAssetCollection, IOwnable {
    // Token ID counter
    uint256 private _tokenIds;

    // Owner of the smart-contract
    address private _owner;

    // Name of the asset collection
    string private _name;

    // Mapping from token ID to owner address
    mapping(uint256 => address) private _owners;

    // Mapping owner address to token count
    mapping(address => uint256) private _balances;

    // Mapping from token ID to token URI
    mapping(uint256 => string) private _tokenURIs;

    constructor(string memory __name) {
        _name = __name;
        _owner = msg.sender;
    }

    modifier onlyOwner() {
        require(_owner == msg.sender, "Ownable: caller is not the owner");
        _;
    }

    function _exists(uint256 tokenId) internal view returns (bool) {
        return _owners[tokenId] != address(0);
    }

    function _setTokenURI(uint tokenId, string memory _tokenURI) internal {
        _tokenURIs[tokenId] = _tokenURI;
    }

    function _mint(address to, uint256 tokenId) internal {
        require(to != address(0), "ERC721: mint to the zero address");
        // Check that tokenId was not minted
        require(!_exists(tokenId), "ERC721: token already minted");

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
    }

    function _transfer(
        address from,
        address to,
        uint256 tokenId
    ) internal {
        require(
            ownerOf(tokenId) == from,
            "ERC721: transfer from incorrect owner"
        );
        require(to != address(0), "ERC721: transfer to the zero address");

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    function _burn(uint256 tokenId) internal {
        address tokenOwner = ownerOf(tokenId);
        _balances[tokenOwner] -= 1;

        emit Transfer(tokenOwner, address(0), tokenId);
    }

    // Creates token
    function mint(address tokenHolder, string memory _tokenURI)
        public
        onlyOwner
    {
        uint256 tokenId = _tokenIds;
        _tokenIds += 1;
        _setTokenURI(tokenId, _tokenURI);
        _mint(tokenHolder, tokenId);
    }

    // Destroys token
    function burn(address tokenHolder, uint256 tokenId) public onlyOwner {
        _mint(tokenHolder, tokenId);
    }

    // Transfer the token back to the DAO
    function transfer(uint256 tokenId) public {
        _transfer(msg.sender, _owner, tokenId);
    }

    // Moves a token from one user to another
    function transferFrom(
        address tokenHolder,
        address to,
        uint256 tokenId
    ) public onlyOwner {
        _transfer(tokenHolder, to, tokenId);
    }

    // Returns how many tokens a user owns
    function balanceOf(address tokenOwner) public view returns (uint256) {
        require(
            tokenOwner != address(0),
            "ERC721: address zero is not a valid owner"
        );
        return _balances[tokenOwner];
    }

    // Returns how many tokens a user owns
    function tokensOf(address tokenOwner)
        public
        view
        returns (uint256[] memory)
    {
        require(
            tokenOwner != address(0),
            "ERC721: address zero is not a valid owner"
        );

        uint256 balance = balanceOf(tokenOwner);
        uint256[] memory tokens = new uint[](balance);
        uint256 index = 0;

        for (uint id = 0; id < _tokenIds; id++) {
            if (_owners[id] == tokenOwner) {
                tokens[index] = id;
                index += 1;
            }
        }

        return tokens;
    }

    // Returns the token URI of the token
    function tokenURI(uint256 tokenId) public view returns (string memory) {
        require(_exists(tokenId), "ERC721: token hasn't been minted");
        return _tokenURIs[_tokenIds];
    }

    // Returns the owner of the token
    function ownerOf(uint256 tokenId) public view returns (address) {
        address tokenOwner = _owners[tokenId];
        require(tokenOwner != address(0), "ERC721: invalid token ID");
        return tokenOwner;
    }

    // Transfers the ownership of the smart-contract
    function transferOwnership(address _newOwner) public {
        require(msg.sender == _owner, "Ownable: Caller is not owner");
        require(
            _newOwner == address(0),
            "Ownable: New owner can not be the zero address"
        );

        address olOwner = _owner;
        _owner = _newOwner;
        emit TransferOwnership(olOwner, _newOwner);
    }

    // Returns name of the collection
    function name() public view returns (string memory) {
        return _name;
    }

    // Returns owner of the smart-contract
    function owner() public view returns (address) {
        return _owner;
    }
}
Enter fullscreen mode Exit fullscreen mode

This token allows us to track an asset securely and transparently.

This is only contract is just for demonstration purposes and is not optimized nor audited.

Asset Collection smart contract deployed on the Avalanche Testnet

Top comments (0)